Skip to content

PlatformLogger

Effect’s logging system generally writes messages to the console by default. However, you might prefer to store logs in a file for easier debugging or archiving. The PlatformLogger.toFile function creates a logger that sends log messages to a file on disk.

Creates a new logger from an existing string-based logger, writing its output to the specified file.

If you include a batchWindow duration when calling toFile, logs are batched for that period before being written. This can reduce overhead if your application produces many log entries. Without a batchWindow, logs are written as they arrive.

Note that toFile returns an Effect that may fail with a PlatformError if the file cannot be opened or written to. Be sure to handle this possibility if you need to react to file I/O issues.

Example (Directing Logs to a File)

This logger requires a FileSystem implementation to open and write to the file. For Node.js, you can use NodeFileSystem.layer.

import {
import PlatformLogger
PlatformLogger
} from "@effect/platform"
import {
import NodeFileSystem
NodeFileSystem
} from "@effect/platform-node"
import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Layer
Layer
,
import Logger
Logger
} from "effect"
// Create a string-based logger (logfmtLogger in this case)
const
const myStringLogger: Logger.Logger<unknown, string>
myStringLogger
=
import Logger
Logger
.
const logfmtLogger: Logger.Logger<unknown, string>

This logger outputs logs in a human-readable format that is easy to read during development or in a production console.

@example

import { Effect, Logger } from "effect"
const program = Effect.log("message1", "message2").pipe(
Effect.annotateLogs({ key1: "value1", key2: "value2" }),
Effect.withLogSpan("myspan")
)
// Effect.runFork(program.pipe(Effect.provide(Logger.logFmt)))
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 myspan=0ms key2=value2 key1=value1

@since2.0.0

logfmtLogger
// Apply toFile to write logs to "/tmp/log.txt"
const
const fileLogger: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>
fileLogger
=
const myStringLogger: Logger.Logger<unknown, string>
myStringLogger
.
Pipeable.pipe<Logger.Logger<unknown, string>, Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>>(this: Logger.Logger<...>, ab: (_: Logger.Logger<unknown, string>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import PlatformLogger
PlatformLogger
.
const toFile: (path: string, options?: (OpenFileOptions & {
readonly batchWindow?: DurationInput | undefined;
}) | undefined) => <Message>(self: Logger.Logger<Message, string>) => Effect.Effect<Logger.Logger<Message, void>, PlatformError, Scope | FileSystem> (+1 overload)

Create a Logger from another string Logger that writes to the specified file.

@since1.0.0

@example

import { PlatformLogger } from "@effect/platform"
import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Logger } from "effect"
const fileLogger = Logger.logfmtLogger.pipe(
PlatformLogger.toFile("/tmp/log.txt")
)
const LoggerLive = Logger.replaceScoped(Logger.defaultLogger, fileLogger).pipe(
Layer.provide(NodeFileSystem.layer)
)
Effect.log("a").pipe(
Effect.zipRight(Effect.log("b")),
Effect.zipRight(Effect.log("c")),
Effect.provide(LoggerLive),
NodeRuntime.runMain
)

toFile
("/tmp/log.txt")
)
// Replace the default logger, providing NodeFileSystem
// to access the file system
const
const LoggerLive: Layer.Layer<never, PlatformError, never>
LoggerLive
=
import Logger
Logger
.
const replaceScoped: <void, void, PlatformError, Scope | FileSystem>(self: Logger.Logger<unknown, void>, that: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>) => Layer.Layer<...> (+1 overload)

@since2.0.0

replaceScoped
(
import Logger
Logger
.
const defaultLogger: Logger.Logger<unknown, void>

@since2.0.0

defaultLogger
,
const fileLogger: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>
fileLogger
).
Pipeable.pipe<Layer.Layer<never, PlatformError, FileSystem>, Layer.Layer<never, PlatformError, never>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<never, PlatformError, FileSystem>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <never, never, FileSystem>(that: Layer.Layer<FileSystem, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<...> (+3 overloads)

Feeds the output services of this builder into the input of the specified builder, resulting in a new builder with the inputs of this builder as well as any leftover inputs, and the outputs of the specified builder.

@since2.0.0

provide
(
import NodeFileSystem
NodeFileSystem
.
const layer: Layer.Layer<FileSystem, never, never>

@since1.0.0

layer
))
const
const program: Effect.Effect<void, never, never>
program
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level, which is INFO by default. This function allows logging multiple items at once and can include detailed error information using Cause instances.

To adjust the log level, use the Logger.withMinimumLogLevel function.

@example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
// Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
("Hello")
// Run the program, writing logs to /tmp/log.txt
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runFork: <void, PlatformError>(effect: Effect.Effect<void, PlatformError, never>, options?: RunForkOptions) => RuntimeFiber<...>

The foundational function for running effects, returning a "fiber" that can be observed or interrupted.

When to Use

runFork is used to run an effect in the background by creating a fiber. It is the base function for all other run functions. It starts a fiber that can be observed or interrupted.

Unless you specifically need a Promise or synchronous operation, runFork is a good default choice.

@example

// Title: Running an Effect in the Background
import { Effect, Console, Schedule, Fiber } from "effect"
// ┌─── Effect<number, never, never>
// ▼
const program = Effect.repeat(
Console.log("running..."),
Schedule.spaced("200 millis")
)
// ┌─── RuntimeFiber<number, never>
// ▼
const fiber = Effect.runFork(program)
setTimeout(() => {
Effect.runFork(Fiber.interrupt(fiber))
}, 500)

@since2.0.0

runFork
(
const program: Effect.Effect<void, never, never>
program
.
Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, PlatformError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, PlatformError, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const provide: <never, PlatformError, never>(layer: Layer.Layer<never, PlatformError, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<...> (+9 overloads)

Provides the necessary Layers to an effect, removing its dependency on the environment.

You can pass multiple layers, a Context, Runtime, or ManagedRuntime to the effect.

@seeprovideService for providing a service to an effect.

@example

import { Context, Effect, Layer } from "effect"
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}
const DatabaseLive = Layer.succeed(
Database,
{
// Simulate a database query
query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([]))
}
)
// ┌─── Effect<unknown[], never, Database>
// ▼
const program = Effect.gen(function*() {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
return result
})
// ┌─── Effect<unknown[], never, never>
// ▼
const runnable = Effect.provide(program, DatabaseLive)
Effect.runPromise(runnable).then(console.log)
// Output:
// timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users"
// []

@since2.0.0

provide
(
const LoggerLive: Layer.Layer<never, PlatformError, never>
LoggerLive
)))
/*
Logs will be written to "/tmp/log.txt" in the logfmt format,
and won't appear on the console.
*/

In the following example, logs are written to both the console and a file. The console uses the pretty logger, while the file uses the logfmt format.

Example (Directing Logs to Both a File and the Console)

import {
import PlatformLogger
PlatformLogger
} from "@effect/platform"
import {
import NodeFileSystem
NodeFileSystem
} from "@effect/platform-node"
import {
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
,
import Layer
Layer
,
import Logger
Logger
} from "effect"
const
const fileLogger: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>
fileLogger
=
import Logger
Logger
.
const logfmtLogger: Logger.Logger<unknown, string>

This logger outputs logs in a human-readable format that is easy to read during development or in a production console.

@example

import { Effect, Logger } from "effect"
const program = Effect.log("message1", "message2").pipe(
Effect.annotateLogs({ key1: "value1", key2: "value2" }),
Effect.withLogSpan("myspan")
)
// Effect.runFork(program.pipe(Effect.provide(Logger.logFmt)))
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 myspan=0ms key2=value2 key1=value1

@since2.0.0

logfmtLogger
.
Pipeable.pipe<Logger.Logger<unknown, string>, Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>>(this: Logger.Logger<...>, ab: (_: Logger.Logger<unknown, string>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import PlatformLogger
PlatformLogger
.
const toFile: (path: string, options?: (OpenFileOptions & {
readonly batchWindow?: DurationInput | undefined;
}) | undefined) => <Message>(self: Logger.Logger<Message, string>) => Effect.Effect<Logger.Logger<Message, void>, PlatformError, Scope | FileSystem> (+1 overload)

Create a Logger from another string Logger that writes to the specified file.

@since1.0.0

@example

import { PlatformLogger } from "@effect/platform"
import { NodeFileSystem, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Logger } from "effect"
const fileLogger = Logger.logfmtLogger.pipe(
PlatformLogger.toFile("/tmp/log.txt")
)
const LoggerLive = Logger.replaceScoped(Logger.defaultLogger, fileLogger).pipe(
Layer.provide(NodeFileSystem.layer)
)
Effect.log("a").pipe(
Effect.zipRight(Effect.log("b")),
Effect.zipRight(Effect.log("c")),
Effect.provide(LoggerLive),
NodeRuntime.runMain
)

toFile
("/tmp/log.txt")
)
// Combine the pretty logger for console output with the file logger
const
const bothLoggers: Effect.Effect<Logger.Logger<unknown, [void, void]>, PlatformError, Scope | FileSystem>
bothLoggers
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const map: <Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem, Logger.Logger<unknown, [void, void]>>(self: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>, f: (a: Logger.Logger<...>) => Logger.Logger<...>) => Effect.Effect<...> (+1 overload)

Transforms the value inside an effect by applying a function to it.

Syntax

const mappedEffect = pipe(myEffect, Effect.map(transformation))
// or
const mappedEffect = Effect.map(myEffect, transformation)
// or
const mappedEffect = myEffect.pipe(Effect.map(transformation))

Details

map takes a function and applies it to the value contained within an effect, creating a new effect with the transformed value.

It's important to note that effects are immutable, meaning that the original effect is not modified. Instead, a new effect is returned with the updated value.

@seemapError for a version that operates on the error channel.

@seemapBoth for a version that operates on both channels.

@seeflatMap or andThen for a version that can return a new effect.

@example

// Title: Adding a Service Charge
import { pipe, Effect } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const finalAmount = pipe(
fetchTransactionAmount,
Effect.map(addServiceCharge)
)
Effect.runPromise(finalAmount).then(console.log)
// Output: 101

@since2.0.0

map
(
const fileLogger: Effect.Effect<Logger.Logger<unknown, void>, PlatformError, Scope | FileSystem>
fileLogger
, (
fileLogger: Logger.Logger<unknown, void>
fileLogger
) =>
import Logger
Logger
.
const zip: <unknown, void, unknown, void>(self: Logger.Logger<unknown, void>, that: Logger.Logger<unknown, void>) => Logger.Logger<unknown, [void, void]> (+1 overload)

Combines this logger with the specified logger to produce a new logger that logs to both this logger and that logger.

@since2.0.0

zip
(
import Logger
Logger
.
const prettyLoggerDefault: Logger.Logger<unknown, void>

A default version of the pretty logger.

@since3.8.0

prettyLoggerDefault
,
fileLogger: Logger.Logger<unknown, void>
fileLogger
)
)
const
const LoggerLive: Layer.Layer<never, PlatformError, never>
LoggerLive
=
import Logger
Logger
.
const replaceScoped: <void, [void, void], PlatformError, Scope | FileSystem>(self: Logger.Logger<unknown, void>, that: Effect.Effect<Logger.Logger<unknown, [void, void]>, PlatformError, Scope | FileSystem>) => Layer.Layer<...> (+1 overload)

@since2.0.0

replaceScoped
(
import Logger
Logger
.
const defaultLogger: Logger.Logger<unknown, void>

@since2.0.0

defaultLogger
,
const bothLoggers: Effect.Effect<Logger.Logger<unknown, [void, void]>, PlatformError, Scope | FileSystem>
bothLoggers
).
Pipeable.pipe<Layer.Layer<never, PlatformError, FileSystem>, Layer.Layer<never, PlatformError, never>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<never, PlatformError, FileSystem>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <never, never, FileSystem>(that: Layer.Layer<FileSystem, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<...> (+3 overloads)

Feeds the output services of this builder into the input of the specified builder, resulting in a new builder with the inputs of this builder as well as any leftover inputs, and the outputs of the specified builder.

@since2.0.0

provide
(
import NodeFileSystem
NodeFileSystem
.
const layer: Layer.Layer<FileSystem, never, never>

@since1.0.0

layer
))
const
const program: Effect.Effect<void, never, never>
program
=
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void, never, never>

Logs one or more messages or error causes at the current log level, which is INFO by default. This function allows logging multiple items at once and can include detailed error information using Cause instances.

To adjust the log level, use the Logger.withMinimumLogLevel function.

@example

import { Cause, Effect } from "effect"
const program = Effect.log(
"message1",
"message2",
Cause.die("Oh no!"),
Cause.die("Oh uh!")
)
// Effect.runFork(program)
// Output:
// timestamp=... level=INFO fiber=#0 message=message1 message=message2 cause="Error: Oh no!
// Error: Oh uh!"

@since2.0.0

log
("Hello")
// Run the program, writing logs to both the console (pretty format)
// and "/tmp/log.txt" (logfmt)
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const runFork: <void, PlatformError>(effect: Effect.Effect<void, PlatformError, never>, options?: RunForkOptions) => RuntimeFiber<...>

The foundational function for running effects, returning a "fiber" that can be observed or interrupted.

When to Use

runFork is used to run an effect in the background by creating a fiber. It is the base function for all other run functions. It starts a fiber that can be observed or interrupted.

Unless you specifically need a Promise or synchronous operation, runFork is a good default choice.

@example

// Title: Running an Effect in the Background
import { Effect, Console, Schedule, Fiber } from "effect"
// ┌─── Effect<number, never, never>
// ▼
const program = Effect.repeat(
Console.log("running..."),
Schedule.spaced("200 millis")
)
// ┌─── RuntimeFiber<number, never>
// ▼
const fiber = Effect.runFork(program)
setTimeout(() => {
Effect.runFork(Fiber.interrupt(fiber))
}, 500)

@since2.0.0

runFork
(
const program: Effect.Effect<void, never, never>
program
.
Pipeable.pipe<Effect.Effect<void, never, never>, Effect.Effect<void, PlatformError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<void, never, never>) => Effect.Effect<void, PlatformError, never>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect

@since2.0.0

@since2.0.0

@since2.0.0

Effect
.
const provide: <never, PlatformError, never>(layer: Layer.Layer<never, PlatformError, never>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<...> (+9 overloads)

Provides the necessary Layers to an effect, removing its dependency on the environment.

You can pass multiple layers, a Context, Runtime, or ManagedRuntime to the effect.

@seeprovideService for providing a service to an effect.

@example

import { Context, Effect, Layer } from "effect"
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}
const DatabaseLive = Layer.succeed(
Database,
{
// Simulate a database query
query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([]))
}
)
// ┌─── Effect<unknown[], never, Database>
// ▼
const program = Effect.gen(function*() {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
return result
})
// ┌─── Effect<unknown[], never, never>
// ▼
const runnable = Effect.provide(program, DatabaseLive)
Effect.runPromise(runnable).then(console.log)
// Output:
// timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users"
// []

@since2.0.0

provide
(
const LoggerLive: Layer.Layer<never, PlatformError, never>
LoggerLive
)))