/** * Defines loggers and log-event data for Effect programs. * * A `Logger` receives each log event as `Options` and turns it * into output such as a formatted string, structured object, console write, * file write, JSON line, or trace span event. This module also includes active * logger references, console routing helpers, built-in formatters, batching, * file logging, and layers for installing loggers. * * @stability stable * @since 2.0.0 */ import * as Array from "./Array.ts" import type * as Cause from "./Cause.ts" import type * as Context from "./Context.ts" import type * as Duration from "./Duration.ts" import type * as Effect from "./Effect.ts" import type * as Fiber from "./Fiber.ts" import * as FileSystem from "./FileSystem.ts" import * as Formatter from "./Formatter.ts" import { dual } from "./Function.ts" import { isEffect, withFiber } from "./internal/core.ts" import * as effect from "./internal/effect.ts" import * as InternalRecord from "./internal/record.ts" import * as Layer from "./Layer.ts" import type * as LogLevel from "./LogLevel.ts" import type { Pipeable } from "./Pipeable.ts" import type { PlatformError } from "./PlatformError.ts" import * as Predicate from "./Predicate.ts" import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts" import type * as Scope from "./Scope.ts" const TypeId = "~effect/Logger" /** * A logger that transforms a runtime log event into an output value. * * **Details** * * The runtime calls `log` with the message, level, cause, fiber, and timestamp * for each log event. Use `Logger.layer` to install one or more loggers for an * effect. * * **Example** (Creating custom loggers) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const messages: Array = [] * const stringLogger = Logger.make((options) => { * messages.push(`[${options.logLevel}] ${options.message}`) * }) * * const program = Effect.log("Hello World").pipe( * Effect.provide(Logger.layer([stringLogger])) * ) * * Effect.runSync(program) * messages // => ["[Info] Hello World"] * ``` * * @stability stable * @category models * @since 2.0.0 */ export interface Logger extends Pipeable { readonly [TypeId]: typeof TypeId log(options: Options): Output } /** * Information supplied to a `Logger` for a single log event. * * **Details** * * Includes the logged message, log level, cause, current fiber, and timestamp. * * **Example** (Accessing logger options) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const outputs: Array = [] * const detailedLogger = Logger.make((options) => { * outputs.push({ * message: options.message, * level: options.logLevel, * hasCause: options.cause.reasons.length > 0 * }) * }) * * const program = Effect.log("Processing request").pipe( * Effect.provide(Logger.layer([detailedLogger])) * ) * * Effect.runSync(program) * outputs // => [{ message: ["Processing request"], level: "Info", hasCause: false }] * ``` * * @stability stable * @category options * @since 2.0.0 */ export interface Options { readonly message: Message readonly logLevel: LogLevel.LogLevel readonly cause: Cause.Cause readonly fiber: Fiber.Fiber readonly date: Date } /** * Returns `true` if the specified value is a `Logger`, otherwise returns `false`. * * **Example** (Checking logger values) * * ```ts import.meta.vitest * import { Logger } from "effect" * * const myLogger = Logger.make(() => undefined) * * Logger.isLogger(myLogger) // => true * Logger.isLogger("not a logger") // => false * Logger.isLogger({ log: () => {} }) // => false * ``` * * @stability stable * @category guards * @since 4.0.0 */ export const isLogger = (u: unknown): u is Logger => Predicate.hasProperty(u, TypeId) /** * Context reference containing the active loggers for the current fiber. * * **Details** * * By default this set includes the default logger and the tracer logger. * Providing `Logger.layer` replaces or merges with this set depending on its * options. * * **Example** (Accessing current loggers) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const messages: Array = [] * const customLogger = Logger.make((options) => { * messages.push(options.message) * }) * const program = Effect.gen(function*() { * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers) * yield* Effect.log("Hello from custom logger").pipe( * Effect.provide(Logger.layer([customLogger])) * ) * return currentLoggers.has(Logger.defaultLogger) * }) * * Effect.runSync(program) // => true * messages // => [["Hello from custom logger"]] * ``` * * @stability stable * @category services * @since 4.0.0 */ export const CurrentLoggers: Context.Reference>> = effect.CurrentLoggers /** * Context reference that routes the built-in default logger and TTY pretty * console logger to stderr. * * **When to use** * * Use to route built-in logger output to stderr while keeping stdout reserved * for protocol messages or data output. * * **Details** * * The reference defaults to `false`. Providing `true` makes the affected * loggers call `console.error` instead of `console.log`. * * @see {@link defaultLogger} for the runtime logger affected by this reference * @see {@link consolePretty} for the TTY-mode pretty console logger affected by this reference * @see {@link withConsoleError} for routing a specific formatter logger to `console.error` * * @stability stable * @category services * @since 4.0.0 */ export const LogToStderr: Context.Reference = effect.LogToStderr /** * Transforms the output of a `Logger` using the provided function. * * **When to use** * * Use when an existing logger's output should be transformed without recreating the * logging logic. * * **Example** (Transforming logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const outputs: Array = [] * const structuredLogger = Logger.make((options) => ({ * message: options.message * })) * * // Transform to uppercase messages * const uppercaseLogger = Logger.map( * structuredLogger, * (output) => ({ ...output, message: String(output.message).toUpperCase() }) * ) * * const collector = Logger.make((options) => outputs.push(uppercaseLogger.log(options))) * const program = Effect.log("hello").pipe(Effect.provide(Logger.layer([collector]))) * Effect.runSync(program) * outputs // => [{ message: "HELLO" }] * ``` * * @stability stable * @category mapping * @since 2.0.0 */ export const map = dual< /** * Transforms the output of a `Logger` using the provided function. * * **When to use** * * Use when an existing logger's output should be transformed without recreating the * logging logic. * * **Example** (Transforming logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const outputs: Array = [] * const structuredLogger = Logger.make((options) => ({ * message: options.message * })) * * // Transform to uppercase messages * const uppercaseLogger = Logger.map( * structuredLogger, * (output) => ({ ...output, message: String(output.message).toUpperCase() }) * ) * * const collector = Logger.make((options) => outputs.push(uppercaseLogger.log(options))) * const program = Effect.log("hello").pipe(Effect.provide(Logger.layer([collector]))) * Effect.runSync(program) * outputs // => [{ message: "HELLO" }] * ``` * * @stability stable * @category mapping * @since 2.0.0 */ (f: (output: Output) => Output2) => ( self: Logger ) => Logger, /** * Transforms the output of a `Logger` using the provided function. * * **When to use** * * Use when an existing logger's output should be transformed without recreating the * logging logic. * * **Example** (Transforming logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const outputs: Array = [] * const structuredLogger = Logger.make((options) => ({ * message: options.message * })) * * // Transform to uppercase messages * const uppercaseLogger = Logger.map( * structuredLogger, * (output) => ({ ...output, message: String(output.message).toUpperCase() }) * ) * * const collector = Logger.make((options) => outputs.push(uppercaseLogger.log(options))) * const program = Effect.log("hello").pipe(Effect.provide(Logger.layer([collector]))) * Effect.runSync(program) * outputs // => [{ message: "HELLO" }] * ``` * * @stability stable * @category mapping * @since 2.0.0 */ (self: Logger, f: (output: Output) => Output2) => Logger >(2, (self, f) => effect.loggerMake((options) => f(self.log(options)))) /** * Returns a new `Logger` that writes all output of the specified `Logger` to * the console using `console.log`. * * **When to use** * * Use when a logger's string or object output should be routed to `console.log` for * development or debugging. * * **Example** (Writing logger output with console.log) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * import { TestConsole } from "effect/testing" * * // Create a custom formatter * const customFormatter = Logger.make((options) => * `${options.logLevel}: ${options.message}` * ) * * const consoleLogger = Logger.withConsoleLog(customFormatter) * * const program = Effect.gen(function*() { * yield* Effect.log("Hello World").pipe(Effect.provide(Logger.layer([consoleLogger]))) * return yield* TestConsole.logLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => ["Info: Hello World"] * ``` * * @stability stable * @category logging * @since 2.0.0 */ export const withConsoleLog = ( self: Logger ): Logger => effect.loggerMake((options) => { const console = options.fiber.getRef(effect.ConsoleRef) return console.log(self.log(options)) }) /** * Returns a new `Logger` that writes all output of the specified `Logger` to * the console using `console.error`. * * **When to use** * * Use when logger output should be routed to `console.error`, such as error logs that * should appear on stderr instead of stdout. * * **Example** (Writing logger output with console.error) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * import { TestConsole } from "effect/testing" * * // Create an error-specific formatter * const errorFormatter = Logger.make((options) => * `ERROR: ${options.message}` * ) * * const errorLogger = Logger.withConsoleError(errorFormatter) * * const program = Effect.gen(function*() { * yield* Effect.logError("Database connection failed").pipe(Effect.provide(Logger.layer([errorLogger]))) * return yield* TestConsole.errorLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => ["ERROR: Database connection failed"] * ``` * * @stability stable * @category logging * @since 2.0.0 */ export const withConsoleError = ( self: Logger ): Logger => effect.loggerMake((options) => { const console = options.fiber.getRef(effect.ConsoleRef) return console.error(self.log(options)) }) /** * Returns a new `Logger` that writes all output of the specified `Logger` to * the console. * * **Details** * * Will use the appropriate console method (i.e. `console.log`, `console.error`, * etc.) based upon the current `LogLevel`. * * `Debug` uses `console.debug`, `Info` uses `console.info`, `Trace` uses * `console.trace`, `Warn` uses `console.warn`, `Error` and `Fatal` use * `console.error`, and all other levels use `console.log`. * * **Example** (Writing logs with level-based console methods) * * ```ts import.meta.vitest * import { Console, Effect, Logger } from "effect" * * const messages: Array> = [] * const testConsole: Console.Console = Object.assign(Object.create(console), { * info: (message: unknown) => messages.push(["info", message]), * warn: (message: unknown) => messages.push(["warn", message]), * error: (message: unknown) => messages.push(["error", message]) * }) * const formatter = Logger.make((options) => * `[${options.logLevel}] ${options.message}` * ) * * const leveledLogger = Logger.withLeveledConsole(formatter) * * const program = Effect.gen(function*() { * yield* Effect.logInfo("Info message") // -> console.info * yield* Effect.logWarning("Warning") // -> console.warn * yield* Effect.logError("Error occurred") // -> console.error * }).pipe(Effect.provide(Logger.layer([leveledLogger]))) * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) * const expected = [ * ["info", "[Info] Info message"], * ["warn", "[Warn] Warning"], * ["error", "[Error] Error occurred"] * ] * messages // => expected * ``` * * @stability stable * @category logging * @since 3.8.0 */ export const withLeveledConsole = ( self: Logger ): Logger => effect.loggerMake((options) => { const console = options.fiber.getRef(effect.ConsoleRef) const output = self.log(options) switch (options.logLevel) { case "Debug": return console.debug(output) case "Info": return console.info(output) case "Trace": return console.trace(output) case "Warn": return console.warn(output) case "Error": case "Fatal": return console.error(output) default: return console.log(output) } }) /** * Match strings that do not contain any whitespace characters, double quotes, * or equal signs. */ const textOnly = /^[^\s"=]*$/ /** * Escapes double quotes in a string. */ const escapeDoubleQuotes = (s: string) => `"${s.replace(/\\([\s\S])|(")/g, "\\$1$2")}"` /** * Formats the identifier of a `Fiber` by prefixing it with a hash tag. */ const formatFiberId = (fiberId: number) => `#${fiberId}` /** * Used by both {@link formatSimple} and {@link formatLogFmt} to render a log * message. * * @internal */ const format = ( quoteValue: (s: string) => string, space?: number | string | undefined ) => ({ cause, date, fiber, logLevel, message }: Options): string => { const formatUnknown = (value: unknown): string => typeof value === "string" ? value : Formatter.format(value, { space }) const formatValue = (value: string): string => value.match(textOnly) ? value : quoteValue(value) const format = (label: string, value: string): string => `${effect.formatLabel(label)}=${formatValue(value)}` const append = (label: string, value: string): string => " " + format(label, value) let out = format("timestamp", date.toISOString()) out += append("level", logLevel.toUpperCase()) out += append("fiber", formatFiberId(fiber.id)) const messages = Array.ensure(message) for (let i = 0; i < messages.length; i++) { out += append("message", formatUnknown(messages[i])) } if (cause.reasons.length > 0) { out += append("cause", effect.causePretty(cause)) } const now = date.getTime() const spans = fiber.getRef(CurrentLogSpans) for (const span of spans) { out += " " + effect.formatLogSpan(span, now) } const annotations = fiber.getRef(CurrentLogAnnotations) for (const [label, value] of Object.entries(annotations)) { out += append(label, formatUnknown(value)) } return out } /** * Creates a new `Logger` from a log function. * * **Details** * * The log function receives an options object containing the message, log level, * cause, fiber information, and timestamp, and should return the desired output. * * **Example** (Creating loggers from functions) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const outputs: Array = [] * const textLogger = Logger.make((options) => * `${options.logLevel}: ${options.message}` * ) * const collector = Logger.make((options) => outputs.push(textLogger.log(options))) * * const program = Effect.log("Hello World").pipe( * Effect.provide(Logger.layer([collector])) * ) * Effect.runSync(program) * outputs // => ["Info: Hello World"] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const make: ( log: (options: Options) => Output ) => Logger = effect.loggerMake /** * The default logging implementation used by the Effect runtime. * * **Example** (Referencing the default logger) * * ```ts import.meta.vitest * import { Logger } from "effect" * * Logger.isLogger(Logger.defaultLogger) // => true * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const defaultLogger: Logger = effect.defaultLogger /** * A `Logger` which outputs logs as a string. * * **Details** * * For example, a simple log entry is rendered as * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`. * * **Example** (Formatting logs as simple strings) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * import { TestConsole } from "effect/testing" * * // Use the simple format logger * const stableSimple = Logger.map(Logger.formatSimple, (output) => * output * .replace(/timestamp=\S+ /, "") * .replace(/fiber=#\d+ /, "") * ) * const program = Effect.gen(function*() { * yield* Effect.log("Application started").pipe( * Effect.provide(Logger.layer([Logger.withConsoleLog(stableSimple)])) * ) * return yield* TestConsole.logLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => ["level=INFO message=\"Application started\""] * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const formatSimple = effect.loggerMake(format(escapeDoubleQuotes)) /** * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) * style. * * **Details** * * For example, a logfmt entry is rendered as * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`. * * **Example** (Formatting logs as logfmt) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * import { TestConsole } from "effect/testing" * * const stableLogFmt = Logger.map(Logger.formatLogFmt, (output) => * output * .replace(/timestamp=\S+ /, "") * .replace(/fiber=#\d+ /, "") * ) * const program = Effect.gen(function*() { * yield* Effect.log("User login").pipe( * Effect.provide(Logger.layer([Logger.withConsoleLog(stableLogFmt)])) * ) * return yield* TestConsole.logLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => ["level=INFO message=\"User login\""] * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const formatLogFmt = effect.loggerMake(format(JSON.stringify, 0)) /** * A `Logger` which outputs logs using a structured format. * * **Details** * * For example, a structured entry can contain `message: [ "hello" ]`, * `level: "INFO"`, `timestamp: "2025-01-03T14:25:39.666Z"`, * `annotations: { key: "value" }`, `spans: { label: 0 }`, and * `fiberId: "#1"`. * * **Example** (Formatting logs as structured objects) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * import { TestConsole } from "effect/testing" * * const stableStructured = Logger.map(Logger.formatStructured, (output) => ({ * message: output.message, * level: output.level * })) * const program = Effect.gen(function*() { * yield* Effect.log("User action").pipe( * Effect.provide(Logger.layer([Logger.withConsoleLog(stableStructured)])) * ) * return yield* TestConsole.logLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => [{ message: "User action", level: "INFO" }] * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const formatStructured: Logger readonly spans: Record }> = effect.loggerMake(({ cause, date, fiber, logLevel, message }) => { const annotationsObj: Record = {} const spansObj: Record = {} const annotations = fiber.getRef(CurrentLogAnnotations) for (const [key, value] of Object.entries(annotations)) { InternalRecord.assignProperty(annotationsObj, key, effect.structuredMessage(value)) } const now = date.getTime() const spans = fiber.getRef(CurrentLogSpans) for (const [label, timestamp] of spans) { InternalRecord.assignProperty(spansObj, label, now - timestamp) } const messageArr = Array.ensure(message) return { message: messageArr.length === 1 ? effect.structuredMessage(messageArr[0]) : messageArr.map(effect.structuredMessage), level: logLevel.toUpperCase(), timestamp: date.toISOString(), cause: cause.reasons.length > 0 ? effect.causePretty(cause) : undefined, annotations: annotationsObj, spans: spansObj, fiberId: formatFiberId(fiber.id) } }) /** * A `Logger` which outputs logs using a structured format serialized as JSON * on a single line. * * **Details** * * For example, a JSON entry can render as `{"message":["hello"],"level":"INFO", * "timestamp":"2025-01-03T14:28:57.508Z","annotations":{"key":"value"}, * "spans":{"label":0},"fiberId":"#1"}`. * * **Example** (Formatting logs as JSON) * * ```ts import.meta.vitest * import { Effect, Formatter, Logger } from "effect" * import { TestConsole } from "effect/testing" * * const stableJson = Logger.map(Logger.formatJson, (json) => { * const output = JSON.parse(json) * return Formatter.formatJson({ message: output.message, level: output.level }) * }) * const program = Effect.gen(function*() { * yield* Effect.log("Server started").pipe( * Effect.provide(Logger.layer([Logger.withConsoleLog(stableJson)])) * ) * return yield* TestConsole.logLines * }).pipe(Effect.provide(TestConsole.layer)) * * await Effect.runPromise(program) // => ["{\"message\":\"Server started\",\"level\":\"INFO\"}"] * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const formatJson = map(formatStructured, Formatter.formatJson) /** * Creates a scoped logger that batches the output of another logger. * * **Details** * * The returned effect starts a scoped background process that periodically * passes buffered outputs to `flush`. When the scope closes, the background * process is interrupted and any remaining buffered entries are flushed. * * **Example** (Batching logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const flushed: Array> = [] * const messageLogger = Logger.make((options) => String(options.message)) * const batchedLogger = Logger.batched(messageLogger, { * window: "1 hour", * flush: (messages) => * Effect.sync(() => { * flushed.push(messages) * }) * }) * * const program = Effect.scoped(Effect.gen(function*() { * const logger = yield* batchedLogger * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger]))) * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger]))) * })) * await Effect.runPromise(program) * flushed // => [["Event 1", "Event 2"]] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const batched = dual< /** * Creates a scoped logger that batches the output of another logger. * * **Details** * * The returned effect starts a scoped background process that periodically * passes buffered outputs to `flush`. When the scope closes, the background * process is interrupted and any remaining buffered entries are flushed. * * **Example** (Batching logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const flushed: Array> = [] * const messageLogger = Logger.make((options) => String(options.message)) * const batchedLogger = Logger.batched(messageLogger, { * window: "1 hour", * flush: (messages) => * Effect.sync(() => { * flushed.push(messages) * }) * }) * * const program = Effect.scoped(Effect.gen(function*() { * const logger = yield* batchedLogger * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger]))) * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger]))) * })) * await Effect.runPromise(program) * flushed // => [["Event 1", "Event 2"]] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ ( options: { readonly window: Duration.Input readonly flush: (messages: Array>) => Effect.Effect } ) => ( self: Logger ) => Effect.Effect, never, Scope.Scope>, /** * Creates a scoped logger that batches the output of another logger. * * **Details** * * The returned effect starts a scoped background process that periodically * passes buffered outputs to `flush`. When the scope closes, the background * process is interrupted and any remaining buffered entries are flushed. * * **Example** (Batching logger output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const flushed: Array> = [] * const messageLogger = Logger.make((options) => String(options.message)) * const batchedLogger = Logger.batched(messageLogger, { * window: "1 hour", * flush: (messages) => * Effect.sync(() => { * flushed.push(messages) * }) * }) * * const program = Effect.scoped(Effect.gen(function*() { * const logger = yield* batchedLogger * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger]))) * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger]))) * })) * await Effect.runPromise(program) * flushed // => [["Event 1", "Event 2"]] * ``` * * @stability stable * @category constructors * @since 2.0.0 */ ( self: Logger, options: { readonly window: Duration.Input readonly flush: (messages: Array>) => Effect.Effect } ) => Effect.Effect, never, Scope.Scope> >(2, ( self: Logger, options: { readonly window: Duration.Input readonly flush: (messages: Array>) => Effect.Effect } ): Effect.Effect, never, Scope.Scope> => effect.suspend(() => { let buffer: Array = [] const flush = effect.uninterruptible(effect.suspend(() => { if (buffer.length === 0) { return effect.void } const arr = buffer buffer = [] return options.flush(arr) })) return effect.uninterruptibleMask((restore) => restore( effect.sleep(options.window).pipe( effect.andThen(flush), effect.forever ) ).pipe( effect.forkDetach, effect.flatMap((fiber) => effect.addFinalizer(() => // Wait for any in-flight flush before draining the remaining buffer. effect.andThen(effect.fiberInterrupt(fiber), flush) ) ), effect.as( effect.loggerMake((options) => { buffer.push(self.log(options)) }) ) ) ) })) /** * A `Logger` which outputs logs in a "pretty" format and writes them to the * console. Chooses between tty and browser implementation. If the runtime * platform is known and fixed, prefer {@link consolePrettyBrowser} or * {@link consolePrettyTty}. * * **Details** * * For example, pretty output can render as * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line * such as `key: value`. * * **Example** (Logging with pretty console output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const prettyLogger = Logger.layer([Logger.consolePretty()]) * * Effect.log("hello").pipe( * Effect.withLogSpan('label'), * Effect.annotateLogs('key', 'value'), * Effect.provide(prettyLogger), * Effect.runSync * ) * ``` * * **Example** (Logging with console.error, when the environment has TTY) * * ```ts import.meta.vitest * import { Effect, Layer, Logger } from "effect" * * const prettyLoggerLayer = Layer.merge( * Logger.layer([Logger.consolePretty()]), * Layer.succeed(Logger.LogToStderr, true) * ) * * Effect.log('hello').pipe( * Effect.provide(prettyLoggerLayer), * Effect.runSync * ) * ``` * * @see {@link consolePrettyBrowser} for browser-specific implementation * @see {@link consolePrettyTty} for the TTY-mode implementation * @stability stable * @category constructors * @since 4.0.0 */ export const consolePretty: ( options?: { readonly colors?: "auto" | boolean | undefined readonly formatDate?: ((date: Date) => string) | undefined readonly mode?: "browser" | "tty" | "auto" | undefined } ) => Logger = effect.consolePretty /** * A `Logger` which outputs logs in a "pretty" format and writes them to the * console. Intended to be used on platforms with a browser console. * * **Details** * * For example, pretty output can render as * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line * such as `key: value`. * * **Example** (Logging with pretty console output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const prettyLogger = Logger.layer([Logger.consolePrettyBrowser()]) * * Effect.log("hello").pipe( * Effect.withLogSpan('label'), * Effect.annotateLogs('key', 'value'), * Effect.provide(prettyLogger), * Effect.runSync * ) * ``` * * @see {@link consolePretty} for the platform-independent implementation * @stability stable * @category constructors * @since 4.0.0 */ export const consolePrettyBrowser: ( options?: { readonly colors?: boolean | undefined readonly formatDate?: ((date: Date) => string) | undefined } ) => Logger = effect.prettyLoggerBrowser /** * A `Logger` which outputs logs in a "pretty" format and writes them to the * console. Intended to be used on platforms with tty console. * * **Details** * * For example, pretty output can render as * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line * such as `key: value`. * * **Example** (Logging with pretty console output) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const prettyLogger = Logger.layer([Logger.consolePrettyTty()]) * * Effect.log("hello").pipe( * Effect.withLogSpan('label'), * Effect.annotateLogs('key', 'value'), * Effect.provide(prettyLogger), * Effect.runSync * ) * ``` * * **Example** (Logging with console.error) * * ```ts import.meta.vitest * import { Effect, Layer, Logger } from "effect" * * const prettyLoggerLayer = Layer.merge( * Logger.layer([Logger.consolePrettyTty()]), * Layer.succeed(Logger.LogToStderr, true) * ) * * Effect.log('hello').pipe( * Effect.provide(prettyLoggerLayer), * Effect.runSync * ) * ``` * * @see {@link consolePretty} for the platform-independent implementation * @stability stable * @category constructors * @since 4.0.0 */ export const consolePrettyTty: ( options?: { readonly colors?: boolean | undefined readonly formatDate?: ((date: Date) => string) | undefined } ) => Logger = effect.prettyLoggerTty /** * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt) * style and writes them to the console. * * **Details** * * For example, a console logfmt entry is rendered as * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=info`. * * **Example** (Logging logfmt output to the console) * * ```ts import.meta.vitest * import { Logger } from "effect" * * Logger.isLogger(Logger.consoleLogFmt) // => true * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const consoleLogFmt: Logger = withConsoleLog(formatLogFmt) /** * A `Logger` which outputs logs using a structured format and writes them to * the console. * * **Details** * * For example, console structured output can contain * `message: [ "info", "message" ]`, `level: "INFO"`, * `timestamp: "2025-01-03T14:25:39.666Z"`, * `annotations: { key: "value" }`, `spans: { label: 0 }`, and * `fiberId: "#1"`. * * **Example** (Logging structured output to the console) * * ```ts import.meta.vitest * import { Logger } from "effect" * * Logger.isLogger(Logger.consoleStructured) // => true * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const consoleStructured: Logger = withConsoleLog(formatStructured) /** * A `Logger` which outputs logs using a structured format serialized as JSON * on a single line and writes them to the console. * * **Details** * * For example, console JSON output can render as * `{"message":["hello"],"level":"INFO","timestamp":"2025-01-03T14:28:57.508Z", * "annotations":{"key":"value"},"spans":{"label":0},"fiberId":"#1"}`. * * **Example** (Logging JSON output to the console) * * ```ts import.meta.vitest * import { Logger } from "effect" * * Logger.isLogger(Logger.consoleJson) // => true * ``` * * @stability stable * @category constructors * @since 4.0.0 */ export const consoleJson: Logger = withConsoleLog(formatJson) /** * A `Logger` which includes log messages as tracer span events. * * **Details** * * This logger integrates logging with distributed tracing by recording * all log messages as events on the current trace span, making them visible * in tracing tools like OpenTelemetry, Jaeger, or Zipkin. * * This logger is included in the default set of loggers for all Effect programs, * so log messages automatically appear as span events unless you override the * default loggers. * * **Example** (Recording logs as trace span events) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const program = Effect.log("span event").pipe( * Effect.withSpan("operation"), * Effect.provide(Logger.layer([Logger.tracerLogger])) * ) * Effect.runSync(program) * ``` * * @stability stable * @category constructors * @since 2.0.0 */ export const tracerLogger: Logger = effect.tracerLogger /** * Creates a `Layer` which will overwrite the current set of loggers with the * specified array of `loggers`. * * **Details** * * If the specified array of `loggers` should be _merged_ with the current set * of loggers (instead of overwriting them), set `mergeWithExisting` to `true`. * * **Example** (Providing logger layers) * * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * * const messages: Array = [] * const customLogger = Logger.make((options) => { * messages.push(options.message) * }) * const CustomLoggerLayer = Logger.layer([customLogger]) * * const program = Effect.log("Application started").pipe( * Effect.provide(CustomLoggerLayer) * ) * Effect.runSync(program) * messages // => [["Application started"]] * ``` * * @stability stable * @category layers * @since 4.0.0 */ export const layer = < const Loggers extends ReadonlyArray | Effect.Effect, any, any>> >( loggers: Loggers, options?: { readonly mergeWithExisting?: boolean | undefined } | undefined ): Layer.Layer< never, Loggers extends readonly [] ? never : Effect.Error, Exclude< Loggers extends readonly [] ? never : Effect.Services, Scope.Scope > > => Layer.effect( CurrentLoggers, withFiber(effect.fnUntraced(function*(fiber) { const currentLoggers = new Set(options?.mergeWithExisting === true ? fiber.getRef(effect.CurrentLoggers) : []) for (const logger of loggers) { currentLoggers.add(isEffect(logger) ? yield* logger : logger) } return currentLoggers })) ) /** * Creates a scoped logger that writes string logger output to a file. * * **Details** * * The returned effect requires `FileSystem` and `Scope`. The file logger batches * string output, writes each batch to the specified path, and flushes remaining * entries when the scope closes. * * **Example** (Writing JSON logs to a file) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt") * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger]))) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["a\nb\nc"] * ``` * * **Example** (Writing logs to files) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", { * batchWindow: "1 hour" * }) * yield* Effect.log("Application started").pipe( * Effect.provide(Logger.layer([fileLogger])) * ) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["Application started"] * ``` * * @stability unstable * @category logging * @since 4.0.0 */ export const toFile = dual< /** * Creates a scoped logger that writes string logger output to a file. * * **Details** * * The returned effect requires `FileSystem` and `Scope`. The file logger batches * string output, writes each batch to the specified path, and flushes remaining * entries when the scope closes. * * **Example** (Writing JSON logs to a file) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt") * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger]))) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["a\nb\nc"] * ``` * * **Example** (Writing logs to files) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", { * batchWindow: "1 hour" * }) * yield* Effect.log("Application started").pipe( * Effect.provide(Logger.layer([fileLogger])) * ) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["Application started"] * ``` * * @stability unstable * @category logging * @since 4.0.0 */ ( path: string, options?: { readonly flag?: FileSystem.OpenFlag | undefined readonly mode?: number | undefined readonly batchWindow?: Duration.Input | undefined } | undefined ) => ( self: Logger ) => Effect.Effect, PlatformError, Scope.Scope | FileSystem.FileSystem>, /** * Creates a scoped logger that writes string logger output to a file. * * **Details** * * The returned effect requires `FileSystem` and `Scope`. The file logger batches * string output, writes each batch to the specified path, and flushes remaining * entries when the scope closes. * * **Example** (Writing JSON logs to a file) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt") * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger]))) * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger]))) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["a\nb\nc"] * ``` * * **Example** (Writing logs to files) * * ```ts import.meta.vitest * import { Effect, FileSystem, Logger } from "effect" * * const writes: Array = [] * const file = { * writeAll: (buffer: Uint8Array) => Effect.sync(() => { * writes.push(new TextDecoder().decode(buffer).trim()) * }) * } as unknown as FileSystem.File * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) * const messageLogger = Logger.make((options) => String(options.message)) * * const program = Effect.scoped(Effect.gen(function*() { * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", { * batchWindow: "1 hour" * }) * yield* Effect.log("Application started").pipe( * Effect.provide(Logger.layer([fileLogger])) * ) * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) * * await Effect.runPromise(program) * writes // => ["Application started"] * ``` * * @stability unstable * @category logging * @since 4.0.0 */ ( self: Logger, path: string, options?: { readonly flag?: FileSystem.OpenFlag | undefined readonly mode?: number | undefined readonly batchWindow?: Duration.Input | undefined } | undefined ) => Effect.Effect, PlatformError, Scope.Scope | FileSystem.FileSystem> >( (args) => isLogger(args[0]), (self, path, options) => effect.gen(function*() { const fs = yield* FileSystem.FileSystem const logFile = yield* fs.open(path, { flag: "a+", ...options }) const encoder = new TextEncoder() return yield* batched(self, { window: options?.batchWindow ?? 1000, flush: (output) => effect.ignore(logFile.writeAll(encoder.encode(output.join("\n") + "\n"))) }) }) )