1/**
2 * Defines loggers and log-event data for Effect programs.
3 *
4 * A `Logger<Message, Output>` receives each log event as `Options` and turns it
5 * into output such as a formatted string, structured object, console write,
6 * file write, JSON line, or trace span event. This module also includes active
7 * logger references, console routing helpers, built-in formatters, batching,
8 * file logging, and layers for installing loggers.
9 *
10 * @since 2.0.0
11 */
12import * as Array from "./Array.ts"
13import type * as Cause from "./Cause.ts"
14import type * as Context from "./Context.ts"
15import type * as Duration from "./Duration.ts"
16import type * as Effect from "./Effect.ts"
17import type * as Fiber from "./Fiber.ts"
18import * as FileSystem from "./FileSystem.ts"
19import * as Formatter from "./Formatter.ts"
20import { dual } from "./Function.ts"
21import { isEffect, withFiber } from "./internal/core.ts"
22import * as effect from "./internal/effect.ts"
23import * as InternalRecord from "./internal/record.ts"
24import * as Layer from "./Layer.ts"
25import type * as LogLevel from "./LogLevel.ts"
26import type { Pipeable } from "./Pipeable.ts"
27import type { PlatformError } from "./PlatformError.ts"
28import * as Predicate from "./Predicate.ts"
29import { CurrentLogAnnotations, CurrentLogSpans } from "./References.ts"
30import type * as Scope from "./Scope.ts"
31
32const TypeId = "~effect/Logger"
33
34/**
35 * A logger that transforms a runtime log event into an output value.
36 *
37 * **Details**
38 *
39 * The runtime calls `log` with the message, level, cause, fiber, and timestamp
40 * for each log event. Use `Logger.layer` to install one or more loggers for an
41 * effect.
42 *
43 * **Example** (Creating custom loggers)
44 *
45 * ```ts import.meta.vitest
46 * import { Effect, Logger } from "effect"
47 *
48 * const messages: Array<string> = []
49 * const stringLogger = Logger.make<unknown, void>((options) => {
50 * messages.push(`[${options.logLevel}] ${options.message}`)
51 * })
52 *
53 * const program = Effect.log("Hello World").pipe(
54 * Effect.provide(Logger.layer([stringLogger]))
55 * )
56 *
57 * Effect.runSync(program)
58 * messages // => ["[Info] Hello World"]
59 * ```
60 *
61 * @category models
62 * @since 2.0.0
63 */
64export interface Logger<in Message, out Output> extends Pipeable {
65 readonly [TypeId]: typeof TypeId
66 log(options: Options<Message>): Output
67}
68
69/**
70 * Information supplied to a `Logger` for a single log event.
71 *
72 * **Details**
73 *
74 * Includes the logged message, log level, cause, current fiber, and timestamp.
75 *
76 * **Example** (Accessing logger options)
77 *
78 * ```ts import.meta.vitest
79 * import { Effect, Logger } from "effect"
80 *
81 * const outputs: Array<unknown> = []
82 * const detailedLogger = Logger.make((options) => {
83 * outputs.push({
84 * message: options.message,
85 * level: options.logLevel,
86 * hasCause: options.cause.reasons.length > 0
87 * })
88 * })
89 *
90 * const program = Effect.log("Processing request").pipe(
91 * Effect.provide(Logger.layer([detailedLogger]))
92 * )
93 *
94 * Effect.runSync(program)
95 * outputs // => [{ message: ["Processing request"], level: "Info", hasCause: false }]
96 * ```
97 *
98 * @category options
99 * @since 2.0.0
100 */
101export interface Options<out Message> {
102 readonly message: Message
103 readonly logLevel: LogLevel.LogLevel
104 readonly cause: Cause.Cause<unknown>
105 readonly fiber: Fiber.Fiber<unknown, unknown>
106 readonly date: Date
107}
108
109/**
110 * Returns `true` if the specified value is a `Logger`, otherwise returns `false`.
111 *
112 * **Example** (Checking logger values)
113 *
114 * ```ts import.meta.vitest
115 * import { Logger } from "effect"
116 *
117 * const myLogger = Logger.make(() => undefined)
118 *
119 * Logger.isLogger(myLogger) // => true
120 * Logger.isLogger("not a logger") // => false
121 * Logger.isLogger({ log: () => {} }) // => false
122 * ```
123 *
124 * @category guards
125 * @since 4.0.0
126 */
127export const isLogger = (u: unknown): u is Logger<unknown, unknown> => Predicate.hasProperty(u, TypeId)
128
129/**
130 * Context reference containing the active loggers for the current fiber.
131 *
132 * **Details**
133 *
134 * By default this set includes the default logger and the tracer logger.
135 * Providing `Logger.layer` replaces or merges with this set depending on its
136 * options.
137 *
138 * **Example** (Accessing current loggers)
139 *
140 * ```ts import.meta.vitest
141 * import { Effect, Logger } from "effect"
142 *
143 * const messages: Array<unknown> = []
144 * const customLogger = Logger.make((options) => {
145 * messages.push(options.message)
146 * })
147 * const program = Effect.gen(function*() {
148 * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers)
149 * yield* Effect.log("Hello from custom logger").pipe(
150 * Effect.provide(Logger.layer([customLogger]))
151 * )
152 * return currentLoggers.has(Logger.defaultLogger)
153 * })
154 *
155 * Effect.runSync(program) // => true
156 * messages // => [["Hello from custom logger"]]
157 * ```
158 *
159 * @category services
160 * @since 4.0.0
161 */
162export const CurrentLoggers: Context.Reference<ReadonlySet<Logger<unknown, any>>> = effect.CurrentLoggers
163
164/**
165 * Context reference that routes the built-in default logger and TTY pretty
166 * console logger to stderr.
167 *
168 * **When to use**
169 *
170 * Use to route built-in logger output to stderr while keeping stdout reserved
171 * for protocol messages or data output.
172 *
173 * **Details**
174 *
175 * The reference defaults to `false`. Providing `true` makes the affected
176 * loggers call `console.error` instead of `console.log`.
177 *
178 * @see {@link defaultLogger} for the runtime logger affected by this reference
179 * @see {@link consolePretty} for the TTY-mode pretty console logger affected by this reference
180 * @see {@link withConsoleError} for routing a specific formatter logger to `console.error`
181 *
182 * @category services
183 * @since 4.0.0
184 */
185export const LogToStderr: Context.Reference<boolean> = effect.LogToStderr
186
187/**
188 * Transforms the output of a `Logger` using the provided function.
189 *
190 * **When to use**
191 *
192 * Use when an existing logger's output should be transformed without recreating the
193 * logging logic.
194 *
195 * **Example** (Transforming logger output)
196 *
197 * ```ts import.meta.vitest
198 * import { Effect, Logger } from "effect"
199 *
200 * const outputs: Array<unknown> = []
201 * const structuredLogger = Logger.make((options) => ({
202 * message: options.message
203 * }))
204 *
205 * // Transform to uppercase messages
206 * const uppercaseLogger = Logger.map(
207 * structuredLogger,
208 * (output) => ({ ...output, message: String(output.message).toUpperCase() })
209 * )
210 *
211 * const collector = Logger.make((options) => outputs.push(uppercaseLogger.log(options)))
212 * const program = Effect.log("hello").pipe(Effect.provide(Logger.layer([collector])))
213 * Effect.runSync(program)
214 * outputs // => [{ message: "HELLO" }]
215 * ```
216 *
217 * @category mapping
218 * @since 2.0.0
219 */
220export const map = dual<
221 <Output, Output2>(
222 f: (output: Output) => Output2
223 ) => <Message>(
224 self: Logger<Message, Output>
225 ) => Logger<Message, Output2>,
226 <Message, Output, Output2>(
227 self: Logger<Message, Output>,
228 f: (output: Output) => Output2
229 ) => Logger<Message, Output2>
230>(2, (self, f) => effect.loggerMake((options) => f(self.log(options))))
231
232/**
233 * Returns a new `Logger` that writes all output of the specified `Logger` to
234 * the console using `console.log`.
235 *
236 * **When to use**
237 *
238 * Use when a logger's string or object output should be routed to `console.log` for
239 * development or debugging.
240 *
241 * **Example** (Writing logger output with console.log)
242 *
243 * ```ts import.meta.vitest
244 * import { Effect, Logger } from "effect"
245 * import { TestConsole } from "effect/testing"
246 *
247 * // Create a custom formatter
248 * const customFormatter = Logger.make((options) =>
249 * `${options.logLevel}: ${options.message}`
250 * )
251 *
252 * const consoleLogger = Logger.withConsoleLog(customFormatter)
253 *
254 * const program = Effect.gen(function*() {
255 * yield* Effect.log("Hello World").pipe(Effect.provide(Logger.layer([consoleLogger])))
256 * return yield* TestConsole.logLines
257 * }).pipe(Effect.provide(TestConsole.layer))
258 *
259 * await Effect.runPromise(program) // => ["Info: Hello World"]
260 * ```
261 *
262 * @category logging
263 * @since 2.0.0
264 */
265export const withConsoleLog = <Message, Output>(
266 self: Logger<Message, Output>
267): Logger<Message, void> =>
268 effect.loggerMake((options) => {
269 const console = options.fiber.getRef(effect.ConsoleRef)
270 return console.log(self.log(options))
271 })
272/**
273 * Returns a new `Logger` that writes all output of the specified `Logger` to
274 * the console using `console.error`.
275 *
276 * **When to use**
277 *
278 * Use when logger output should be routed to `console.error`, such as error logs that
279 * should appear on stderr instead of stdout.
280 *
281 * **Example** (Writing logger output with console.error)
282 *
283 * ```ts import.meta.vitest
284 * import { Effect, Logger } from "effect"
285 * import { TestConsole } from "effect/testing"
286 *
287 * // Create an error-specific formatter
288 * const errorFormatter = Logger.make((options) =>
289 * `ERROR: ${options.message}`
290 * )
291 *
292 * const errorLogger = Logger.withConsoleError(errorFormatter)
293 *
294 * const program = Effect.gen(function*() {
295 * yield* Effect.logError("Database connection failed").pipe(Effect.provide(Logger.layer([errorLogger])))
296 * return yield* TestConsole.errorLines
297 * }).pipe(Effect.provide(TestConsole.layer))
298 *
299 * await Effect.runPromise(program) // => ["ERROR: Database connection failed"]
300 * ```
301 *
302 * @category logging
303 * @since 2.0.0
304 */
305export const withConsoleError = <Message, Output>(
306 self: Logger<Message, Output>
307): Logger<Message, void> =>
308 effect.loggerMake((options) => {
309 const console = options.fiber.getRef(effect.ConsoleRef)
310 return console.error(self.log(options))
311 })
312/**
313 * Returns a new `Logger` that writes all output of the specified `Logger` to
314 * the console.
315 *
316 * **Details**
317 *
318 * Will use the appropriate console method (i.e. `console.log`, `console.error`,
319 * etc.) based upon the current `LogLevel`.
320 *
321 * `Debug` uses `console.debug`, `Info` uses `console.info`, `Trace` uses
322 * `console.trace`, `Warn` uses `console.warn`, `Error` and `Fatal` use
323 * `console.error`, and all other levels use `console.log`.
324 *
325 * **Example** (Writing logs with level-based console methods)
326 *
327 * ```ts import.meta.vitest
328 * import { Console, Effect, Logger } from "effect"
329 *
330 * const messages: Array<ReadonlyArray<unknown>> = []
331 * const testConsole: Console.Console = Object.assign(Object.create(console), {
332 * info: (message: unknown) => messages.push(["info", message]),
333 * warn: (message: unknown) => messages.push(["warn", message]),
334 * error: (message: unknown) => messages.push(["error", message])
335 * })
336 * const formatter = Logger.make((options) =>
337 * `[${options.logLevel}] ${options.message}`
338 * )
339 *
340 * const leveledLogger = Logger.withLeveledConsole(formatter)
341 *
342 * const program = Effect.gen(function*() {
343 * yield* Effect.logInfo("Info message") // -> console.info
344 * yield* Effect.logWarning("Warning") // -> console.warn
345 * yield* Effect.logError("Error occurred") // -> console.error
346 * }).pipe(Effect.provide(Logger.layer([leveledLogger])))
347 * Effect.runSync(Effect.provideService(program, Console.Console, testConsole))
348 * const expected = [
349 * ["info", "[Info] Info message"],
350 * ["warn", "[Warn] Warning"],
351 * ["error", "[Error] Error occurred"]
352 * ]
353 * messages // => expected
354 * ```
355 *
356 * @category logging
357 * @since 3.8.0
358 */
359export const withLeveledConsole = <Message, Output>(
360 self: Logger<Message, Output>
361): Logger<Message, void> =>
362 effect.loggerMake((options) => {
363 const console = options.fiber.getRef(effect.ConsoleRef)
364 const output = self.log(options)
365 switch (options.logLevel) {
366 case "Debug":
367 return console.debug(output)
368 case "Info":
369 return console.info(output)
370 case "Trace":
371 return console.trace(output)
372 case "Warn":
373 return console.warn(output)
374 case "Error":
375 case "Fatal":
376 return console.error(output)
377 default:
378 return console.log(output)
379 }
380 })
381
382/**
383 * Match strings that do not contain any whitespace characters, double quotes,
384 * or equal signs.
385 */
386const textOnly = /^[^\s"=]*$/
387
388/**
389 * Escapes double quotes in a string.
390 */
391const escapeDoubleQuotes = (s: string) => `"${s.replace(/\\([\s\S])|(")/g, "\\$1$2")}"`
392
393/**
394 * Formats the identifier of a `Fiber` by prefixing it with a hash tag.
395 */
396const formatFiberId = (fiberId: number) => `#${fiberId}`
397
398/**
399 * Used by both {@link formatSimple} and {@link formatLogFmt} to render a log
400 * message.
401 *
402 * @internal
403 */
404const format = (
405 quoteValue: (s: string) => string,
406 space?: number | string | undefined
407) =>
408({ cause, date, fiber, logLevel, message }: Options<unknown>): string => {
409 const formatUnknown = (value: unknown): string =>
410 typeof value === "string" ? value : Formatter.format(value, { space })
411 const formatValue = (value: string): string => value.match(textOnly) ? value : quoteValue(value)
412 const format = (label: string, value: string): string => `${effect.formatLabel(label)}=${formatValue(value)}`
413 const append = (label: string, value: string): string => " " + format(label, value)
414
415 let out = format("timestamp", date.toISOString())
416 out += append("level", logLevel.toUpperCase())
417 out += append("fiber", formatFiberId(fiber.id))
418
419 const messages = Array.ensure(message)
420 for (let i = 0; i < messages.length; i++) {
421 out += append("message", formatUnknown(messages[i]))
422 }
423
424 if (cause.reasons.length > 0) {
425 out += append("cause", effect.causePretty(cause))
426 }
427
428 const now = date.getTime()
429 const spans = fiber.getRef(CurrentLogSpans)
430 for (const span of spans) {
431 out += " " + effect.formatLogSpan(span, now)
432 }
433
434 const annotations = fiber.getRef(CurrentLogAnnotations)
435 for (const [label, value] of Object.entries(annotations)) {
436 out += append(label, formatUnknown(value))
437 }
438
439 return out
440}
441
442/**
443 * Creates a new `Logger` from a log function.
444 *
445 * **Details**
446 *
447 * The log function receives an options object containing the message, log level,
448 * cause, fiber information, and timestamp, and should return the desired output.
449 *
450 * **Example** (Creating loggers from functions)
451 *
452 * ```ts import.meta.vitest
453 * import { Effect, Logger } from "effect"
454 *
455 * const outputs: Array<string> = []
456 * const textLogger = Logger.make((options) =>
457 * `${options.logLevel}: ${options.message}`
458 * )
459 * const collector = Logger.make((options) => outputs.push(textLogger.log(options)))
460 *
461 * const program = Effect.log("Hello World").pipe(
462 * Effect.provide(Logger.layer([collector]))
463 * )
464 * Effect.runSync(program)
465 * outputs // => ["Info: Hello World"]
466 * ```
467 *
468 * @category constructors
469 * @since 2.0.0
470 */
471export const make: <Message, Output>(
472 log: (options: Options<Message>) => Output
473) => Logger<Message, Output> = effect.loggerMake
474
475/**
476 * The default logging implementation used by the Effect runtime.
477 *
478 * **Example** (Referencing the default logger)
479 *
480 * ```ts import.meta.vitest
481 * import { Logger } from "effect"
482 *
483 * Logger.isLogger(Logger.defaultLogger) // => true
484 * ```
485 *
486 * @category constructors
487 * @since 2.0.0
488 */
489export const defaultLogger: Logger<unknown, void> = effect.defaultLogger
490
491/**
492 * A `Logger` which outputs logs as a string.
493 *
494 * **Details**
495 *
496 * For example, a simple log entry is rendered as
497 * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`.
498 *
499 * **Example** (Formatting logs as simple strings)
500 *
501 * ```ts import.meta.vitest
502 * import { Effect, Logger } from "effect"
503 * import { TestConsole } from "effect/testing"
504 *
505 * // Use the simple format logger
506 * const stableSimple = Logger.map(Logger.formatSimple, (output) =>
507 * output
508 * .replace(/timestamp=\S+ /, "")
509 * .replace(/fiber=#\d+ /, "")
510 * )
511 * const program = Effect.gen(function*() {
512 * yield* Effect.log("Application started").pipe(
513 * Effect.provide(Logger.layer([Logger.withConsoleLog(stableSimple)]))
514 * )
515 * return yield* TestConsole.logLines
516 * }).pipe(Effect.provide(TestConsole.layer))
517 *
518 * await Effect.runPromise(program) // => ["level=INFO message=\"Application started\""]
519 * ```
520 *
521 * @category constructors
522 * @since 4.0.0
523 */
524export const formatSimple = effect.loggerMake(format(escapeDoubleQuotes))
525
526/**
527 * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt)
528 * style.
529 *
530 * **Details**
531 *
532 * For example, a logfmt entry is rendered as
533 * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=hello`.
534 *
535 * **Example** (Formatting logs as logfmt)
536 *
537 * ```ts import.meta.vitest
538 * import { Effect, Logger } from "effect"
539 * import { TestConsole } from "effect/testing"
540 *
541 * const stableLogFmt = Logger.map(Logger.formatLogFmt, (output) =>
542 * output
543 * .replace(/timestamp=\S+ /, "")
544 * .replace(/fiber=#\d+ /, "")
545 * )
546 * const program = Effect.gen(function*() {
547 * yield* Effect.log("User login").pipe(
548 * Effect.provide(Logger.layer([Logger.withConsoleLog(stableLogFmt)]))
549 * )
550 * return yield* TestConsole.logLines
551 * }).pipe(Effect.provide(TestConsole.layer))
552 *
553 * await Effect.runPromise(program) // => ["level=INFO message=\"User login\""]
554 * ```
555 *
556 * @category constructors
557 * @since 4.0.0
558 */
559export const formatLogFmt = effect.loggerMake(format(JSON.stringify, 0))
560
561/**
562 * A `Logger` which outputs logs using a structured format.
563 *
564 * **Details**
565 *
566 * For example, a structured entry can contain `message: [ "hello" ]`,
567 * `level: "INFO"`, `timestamp: "2025-01-03T14:25:39.666Z"`,
568 * `annotations: { key: "value" }`, `spans: { label: 0 }`, and
569 * `fiberId: "#1"`.
570 *
571 * **Example** (Formatting logs as structured objects)
572 *
573 * ```ts import.meta.vitest
574 * import { Effect, Logger } from "effect"
575 * import { TestConsole } from "effect/testing"
576 *
577 * const stableStructured = Logger.map(Logger.formatStructured, (output) => ({
578 * message: output.message,
579 * level: output.level
580 * }))
581 * const program = Effect.gen(function*() {
582 * yield* Effect.log("User action").pipe(
583 * Effect.provide(Logger.layer([Logger.withConsoleLog(stableStructured)]))
584 * )
585 * return yield* TestConsole.logLines
586 * }).pipe(Effect.provide(TestConsole.layer))
587 *
588 * await Effect.runPromise(program) // => [{ message: "User action", level: "INFO" }]
589 * ```
590 *
591 * @category constructors
592 * @since 4.0.0
593 */
594export const formatStructured: Logger<unknown, {
595 readonly level: string
596 readonly fiberId: string
597 readonly timestamp: string
598 readonly message: unknown
599 readonly cause: string | undefined
600 readonly annotations: Record<string, unknown>
601 readonly spans: Record<string, number>
602}> = effect.loggerMake(({ cause, date, fiber, logLevel, message }) => {
603 const annotationsObj: Record<string, unknown> = {}
604 const spansObj: Record<string, number> = {}
605
606 const annotations = fiber.getRef(CurrentLogAnnotations)
607 for (const [key, value] of Object.entries(annotations)) {
608 InternalRecord.assignProperty(annotationsObj, key, effect.structuredMessage(value))
609 }
610
611 const now = date.getTime()
612 const spans = fiber.getRef(CurrentLogSpans)
613 for (const [label, timestamp] of spans) {
614 InternalRecord.assignProperty(spansObj, label, now - timestamp)
615 }
616
617 const messageArr = Array.ensure(message)
618 return {
619 message: messageArr.length === 1
620 ? effect.structuredMessage(messageArr[0])
621 : messageArr.map(effect.structuredMessage),
622 level: logLevel.toUpperCase(),
623 timestamp: date.toISOString(),
624 cause: cause.reasons.length > 0 ? effect.causePretty(cause) : undefined,
625 annotations: annotationsObj,
626 spans: spansObj,
627 fiberId: formatFiberId(fiber.id)
628 }
629})
630
631/**
632 * A `Logger` which outputs logs using a structured format serialized as JSON
633 * on a single line.
634 *
635 * **Details**
636 *
637 * For example, a JSON entry can render as `{"message":["hello"],"level":"INFO",
638 * "timestamp":"2025-01-03T14:28:57.508Z","annotations":{"key":"value"},
639 * "spans":{"label":0},"fiberId":"#1"}`.
640 *
641 * **Example** (Formatting logs as JSON)
642 *
643 * ```ts import.meta.vitest
644 * import { Effect, Formatter, Logger } from "effect"
645 * import { TestConsole } from "effect/testing"
646 *
647 * const stableJson = Logger.map(Logger.formatJson, (json) => {
648 * const output = JSON.parse(json)
649 * return Formatter.formatJson({ message: output.message, level: output.level })
650 * })
651 * const program = Effect.gen(function*() {
652 * yield* Effect.log("Server started").pipe(
653 * Effect.provide(Logger.layer([Logger.withConsoleLog(stableJson)]))
654 * )
655 * return yield* TestConsole.logLines
656 * }).pipe(Effect.provide(TestConsole.layer))
657 *
658 * await Effect.runPromise(program) // => ["{\"message\":\"Server started\",\"level\":\"INFO\"}"]
659 * ```
660 *
661 * @category constructors
662 * @since 4.0.0
663 */
664export const formatJson = map(formatStructured, Formatter.formatJson)
665
666/**
667 * Creates a scoped logger that batches the output of another logger.
668 *
669 * **Details**
670 *
671 * The returned effect starts a scoped background process that periodically
672 * passes buffered outputs to `flush`. When the scope closes, the background
673 * process is interrupted and any remaining buffered entries are flushed.
674 *
675 * **Example** (Batching logger output)
676 *
677 * ```ts import.meta.vitest
678 * import { Effect, Logger } from "effect"
679 *
680 * const flushed: Array<ReadonlyArray<string>> = []
681 * const messageLogger = Logger.make((options) => String(options.message))
682 * const batchedLogger = Logger.batched(messageLogger, {
683 * window: "1 hour",
684 * flush: (messages) =>
685 * Effect.sync(() => {
686 * flushed.push(messages)
687 * })
688 * })
689 *
690 * const program = Effect.scoped(Effect.gen(function*() {
691 * const logger = yield* batchedLogger
692 * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger])))
693 * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger])))
694 * }))
695 * await Effect.runPromise(program)
696 * flushed // => [["Event 1", "Event 2"]]
697 * ```
698 *
699 * @category constructors
700 * @since 2.0.0
701 */
702export const batched = dual<
703 <Output>(options: {
704 readonly window: Duration.Input
705 readonly flush: (messages: Array<NoInfer<Output>>) => Effect.Effect<void>
706 }) => <Message>(
707 self: Logger<Message, Output>
708 ) => Effect.Effect<Logger<Message, void>, never, Scope.Scope>,
709 <Message, Output>(
710 self: Logger<Message, Output>,
711 options: {
712 readonly window: Duration.Input
713 readonly flush: (messages: Array<NoInfer<Output>>) => Effect.Effect<void>
714 }
715 ) => Effect.Effect<Logger<Message, void>, never, Scope.Scope>
716>(2, <Message, Output>(
717 self: Logger<Message, Output>,
718 options: {
719 readonly window: Duration.Input
720 readonly flush: (messages: Array<NoInfer<Output>>) => Effect.Effect<void>
721 }
722): Effect.Effect<Logger<Message, void>, never, Scope.Scope> =>
723 effect.flatMap(effect.scope, (scope) => {
724 let buffer: Array<Output> = []
725 const flush = effect.suspend(() => {
726 if (buffer.length === 0) {
727 return effect.void
728 }
729 const arr = buffer
730 buffer = []
731 return options.flush(arr)
732 })
733
734 return effect.uninterruptibleMask((restore) =>
735 restore(
736 effect.sleep(options.window).pipe(
737 effect.andThen(flush),
738 effect.forever
739 )
740 ).pipe(
741 effect.forkDetach,
742 effect.flatMap((fiber) => effect.scopeAddFinalizerExit(scope, () => effect.fiberInterrupt(fiber))),
743 effect.andThen(effect.addFinalizer(() => flush)),
744 effect.as(
745 effect.loggerMake((options) => {
746 buffer.push(self.log(options))
747 })
748 )
749 )
750 )
751 }))
752
753/**
754 * A `Logger` which outputs logs in a "pretty" format and writes them to the
755 * console. Chooses between tty and browser implementation. If the runtime
756 * platform is known and fixed, prefer {@link consolePrettyBrowser} or
757 * {@link consolePrettyTty}.
758 *
759 * **Details**
760 *
761 * For example, pretty output can render as
762 * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line
763 * such as `key: value`.
764 *
765 * **Example** (Logging with pretty console output)
766 *
767 * ```ts import.meta.vitest
768 * import { Effect, Logger } from "effect"
769 *
770 * const prettyLogger = Logger.layer([Logger.consolePretty()])
771 *
772 * Effect.log("hello").pipe(
773 * Effect.withLogSpan('label'),
774 * Effect.annotateLogs('key', 'value'),
775 * Effect.provide(prettyLogger),
776 * Effect.runSync
777 * )
778 * ```
779 *
780 * **Example** (Logging with console.error, when the environment has TTY)
781 *
782 * ```ts import.meta.vitest
783 * import { Effect, Layer, Logger } from "effect"
784 *
785 * const prettyLoggerLayer = Layer.merge(
786 * Logger.layer([Logger.consolePretty()]),
787 * Layer.succeed(Logger.LogToStderr, true)
788 * )
789 *
790 * Effect.log('hello').pipe(
791 * Effect.provide(prettyLoggerLayer),
792 * Effect.runSync
793 * )
794 * ```
795 *
796 * @see {@link consolePrettyBrowser} for browser-specific implementation
797 * @see {@link consolePrettyTty} for the TTY-mode implementation
798 * @category constructors
799 * @since 4.0.0
800 */
801export const consolePretty: (
802 options?: {
803 readonly colors?: "auto" | boolean | undefined
804 readonly formatDate?: ((date: Date) => string) | undefined
805 readonly mode?: "browser" | "tty" | "auto" | undefined
806 }
807) => Logger<unknown, void> = effect.consolePretty
808
809/**
810 * A `Logger` which outputs logs in a "pretty" format and writes them to the
811 * console. Intended to be used on platforms with a browser console.
812 *
813 * **Details**
814 *
815 * For example, pretty output can render as
816 * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line
817 * such as `key: value`.
818 *
819 * **Example** (Logging with pretty console output)
820 *
821 * ```ts import.meta.vitest
822 * import { Effect, Logger } from "effect"
823 *
824 * const prettyLogger = Logger.layer([Logger.consolePrettyBrowser()])
825 *
826 * Effect.log("hello").pipe(
827 * Effect.withLogSpan('label'),
828 * Effect.annotateLogs('key', 'value'),
829 * Effect.provide(prettyLogger),
830 * Effect.runSync
831 * )
832 * ```
833 *
834 * @see {@link consolePretty} for the platform-independent implementation
835 * @category constructors
836 * @since 4.0.0
837 */
838export const consolePrettyBrowser: (
839 options?: {
840 readonly colors?: boolean | undefined
841 readonly formatDate?: ((date: Date) => string) | undefined
842 }
843) => Logger<unknown, void> = effect.prettyLoggerBrowser
844
845/**
846 * A `Logger` which outputs logs in a "pretty" format and writes them to the
847 * console. Intended to be used on platforms with tty console.
848 *
849 * **Details**
850 *
851 * For example, pretty output can render as
852 * `[09:37:17.579] INFO (#1) label=0ms: hello` followed by an annotation line
853 * such as `key: value`.
854 *
855 * **Example** (Logging with pretty console output)
856 *
857 * ```ts import.meta.vitest
858 * import { Effect, Logger } from "effect"
859 *
860 * const prettyLogger = Logger.layer([Logger.consolePrettyTty()])
861 *
862 * Effect.log("hello").pipe(
863 * Effect.withLogSpan('label'),
864 * Effect.annotateLogs('key', 'value'),
865 * Effect.provide(prettyLogger),
866 * Effect.runSync
867 * )
868 * ```
869 *
870 * **Example** (Logging with console.error)
871 *
872 * ```ts import.meta.vitest
873 * import { Effect, Layer, Logger } from "effect"
874 *
875 * const prettyLoggerLayer = Layer.merge(
876 * Logger.layer([Logger.consolePrettyTty()]),
877 * Layer.succeed(Logger.LogToStderr, true)
878 * )
879 *
880 * Effect.log('hello').pipe(
881 * Effect.provide(prettyLoggerLayer),
882 * Effect.runSync
883 * )
884 * ```
885 *
886 * @see {@link consolePretty} for the platform-independent implementation
887 * @category constructors
888 * @since 4.0.0
889 */
890export const consolePrettyTty: (
891 options?: {
892 readonly colors?: boolean | undefined
893 readonly formatDate?: ((date: Date) => string) | undefined
894 }
895) => Logger<unknown, void> = effect.prettyLoggerTty
896
897/**
898 * A `Logger` which outputs logs using the [logfmt](https://brandur.org/logfmt)
899 * style and writes them to the console.
900 *
901 * **Details**
902 *
903 * For example, a console logfmt entry is rendered as
904 * `timestamp=2025-01-03T14:22:47.570Z level=INFO fiber=#1 message=info`.
905 *
906 * **Example** (Logging logfmt output to the console)
907 *
908 * ```ts import.meta.vitest
909 * import { Logger } from "effect"
910 *
911 * Logger.isLogger(Logger.consoleLogFmt) // => true
912 * ```
913 *
914 * @category constructors
915 * @since 4.0.0
916 */
917export const consoleLogFmt: Logger<unknown, void> = withConsoleLog(formatLogFmt)
918
919/**
920 * A `Logger` which outputs logs using a structured format and writes them to
921 * the console.
922 *
923 * **Details**
924 *
925 * For example, console structured output can contain
926 * `message: [ "info", "message" ]`, `level: "INFO"`,
927 * `timestamp: "2025-01-03T14:25:39.666Z"`,
928 * `annotations: { key: "value" }`, `spans: { label: 0 }`, and
929 * `fiberId: "#1"`.
930 *
931 * **Example** (Logging structured output to the console)
932 *
933 * ```ts import.meta.vitest
934 * import { Logger } from "effect"
935 *
936 * Logger.isLogger(Logger.consoleStructured) // => true
937 * ```
938 *
939 * @category constructors
940 * @since 4.0.0
941 */
942export const consoleStructured: Logger<unknown, void> = withConsoleLog(formatStructured)
943
944/**
945 * A `Logger` which outputs logs using a structured format serialized as JSON
946 * on a single line and writes them to the console.
947 *
948 * **Details**
949 *
950 * For example, console JSON output can render as
951 * `{"message":["hello"],"level":"INFO","timestamp":"2025-01-03T14:28:57.508Z",
952 * "annotations":{"key":"value"},"spans":{"label":0},"fiberId":"#1"}`.
953 *
954 * **Example** (Logging JSON output to the console)
955 *
956 * ```ts import.meta.vitest
957 * import { Logger } from "effect"
958 *
959 * Logger.isLogger(Logger.consoleJson) // => true
960 * ```
961 *
962 * @category constructors
963 * @since 4.0.0
964 */
965export const consoleJson: Logger<unknown, void> = withConsoleLog(formatJson)
966
967/**
968 * A `Logger` which includes log messages as tracer span events.
969 *
970 * **Details**
971 *
972 * This logger integrates logging with distributed tracing by recording
973 * all log messages as events on the current trace span, making them visible
974 * in tracing tools like OpenTelemetry, Jaeger, or Zipkin.
975 *
976 * This logger is included in the default set of loggers for all Effect programs,
977 * so log messages automatically appear as span events unless you override the
978 * default loggers.
979 *
980 * **Example** (Recording logs as trace span events)
981 *
982 * ```ts import.meta.vitest
983 * import { Effect, Logger } from "effect"
984 *
985 * const program = Effect.log("span event").pipe(
986 * Effect.withSpan("operation"),
987 * Effect.provide(Logger.layer([Logger.tracerLogger]))
988 * )
989 * Effect.runSync(program)
990 * ```
991 *
992 * @category constructors
993 * @since 2.0.0
994 */
995export const tracerLogger: Logger<unknown, void> = effect.tracerLogger
996
997/**
998 * Creates a `Layer` which will overwrite the current set of loggers with the
999 * specified array of `loggers`.
1000 *
1001 * **Details**
1002 *
1003 * If the specified array of `loggers` should be _merged_ with the current set
1004 * of loggers (instead of overwriting them), set `mergeWithExisting` to `true`.
1005 *
1006 * **Example** (Providing logger layers)
1007 *
1008 * ```ts import.meta.vitest
1009 * import { Effect, Logger } from "effect"
1010 *
1011 * const messages: Array<unknown> = []
1012 * const customLogger = Logger.make((options) => {
1013 * messages.push(options.message)
1014 * })
1015 * const CustomLoggerLayer = Logger.layer([customLogger])
1016 *
1017 * const program = Effect.log("Application started").pipe(
1018 * Effect.provide(CustomLoggerLayer)
1019 * )
1020 * Effect.runSync(program)
1021 * messages // => [["Application started"]]
1022 * ```
1023 *
1024 * @category layers
1025 * @since 4.0.0
1026 */
1027export const layer = <
1028 const Loggers extends ReadonlyArray<Logger<unknown, unknown> | Effect.Effect<Logger<unknown, unknown>, any, any>>
1029>(
1030 loggers: Loggers,
1031 options?: { readonly mergeWithExisting?: boolean | undefined } | undefined
1032): Layer.Layer<
1033 never,
1034 Loggers extends readonly [] ? never : Effect.Error<Loggers[number]>,
1035 Exclude<
1036 Loggers extends readonly [] ? never : Effect.Services<Loggers[number]>,
1037 Scope.Scope
1038 >
1039> =>
1040 Layer.effect(
1041 CurrentLoggers,
1042 withFiber(effect.fnUntraced(function*(fiber) {
1043 const currentLoggers = new Set(options?.mergeWithExisting === true ? fiber.getRef(effect.CurrentLoggers) : [])
1044 for (const logger of loggers) {
1045 currentLoggers.add(isEffect(logger) ? yield* logger : logger)
1046 }
1047 return currentLoggers
1048 }))
1049 )
1050
1051/**
1052 * Creates a scoped logger that writes string logger output to a file.
1053 *
1054 * **Details**
1055 *
1056 * The returned effect requires `FileSystem` and `Scope`. The file logger batches
1057 * string output, writes each batch to the specified path, and flushes remaining
1058 * entries when the scope closes.
1059 *
1060 * **Example** (Writing JSON logs to a file)
1061 *
1062 * ```ts import.meta.vitest
1063 * import { Effect, FileSystem, Logger } from "effect"
1064 *
1065 * const writes: Array<string> = []
1066 * const file = {
1067 * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
1068 * writes.push(new TextDecoder().decode(buffer).trim())
1069 * })
1070 * } as unknown as FileSystem.File
1071 * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
1072 * const messageLogger = Logger.make((options) => String(options.message))
1073 *
1074 * const program = Effect.scoped(Effect.gen(function*() {
1075 * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt")
1076 * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger])))
1077 * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger])))
1078 * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger])))
1079 * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
1080 *
1081 * await Effect.runPromise(program)
1082 * writes // => ["a\nb\nc"]
1083 * ```
1084 *
1085 * **Example** (Writing logs to files)
1086 *
1087 * ```ts import.meta.vitest
1088 * import { Effect, FileSystem, Logger } from "effect"
1089 *
1090 * const writes: Array<string> = []
1091 * const file = {
1092 * writeAll: (buffer: Uint8Array) => Effect.sync(() => {
1093 * writes.push(new TextDecoder().decode(buffer).trim())
1094 * })
1095 * } as unknown as FileSystem.File
1096 * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) })
1097 * const messageLogger = Logger.make((options) => String(options.message))
1098 *
1099 * const program = Effect.scoped(Effect.gen(function*() {
1100 * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", {
1101 * batchWindow: "1 hour"
1102 * })
1103 * yield* Effect.log("Application started").pipe(
1104 * Effect.provide(Logger.layer([fileLogger]))
1105 * )
1106 * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem))
1107 *
1108 * await Effect.runPromise(program)
1109 * writes // => ["Application started"]
1110 * ```
1111 *
1112 * @category logging
1113 * @since 4.0.0
1114 */
1115export const toFile = dual<
1116 (
1117 path: string,
1118 options?: {
1119 readonly flag?: FileSystem.OpenFlag | undefined
1120 readonly mode?: number | undefined
1121 readonly batchWindow?: Duration.Input | undefined
1122 } | undefined
1123 ) => <Message>(
1124 self: Logger<Message, string>
1125 ) => Effect.Effect<Logger<Message, void>, PlatformError, Scope.Scope | FileSystem.FileSystem>,
1126 <Message>(
1127 self: Logger<Message, string>,
1128 path: string,
1129 options?: {
1130 readonly flag?: FileSystem.OpenFlag | undefined
1131 readonly mode?: number | undefined
1132 readonly batchWindow?: Duration.Input | undefined
1133 } | undefined
1134 ) => Effect.Effect<Logger<Message, void>, PlatformError, Scope.Scope | FileSystem.FileSystem>
1135>(
1136 (args) => isLogger(args[0]),
1137 (self, path, options) =>
1138 effect.gen(function*() {
1139 const fs = yield* FileSystem.FileSystem
1140 const logFile = yield* fs.open(path, { flag: "a+", ...options })
1141 const encoder = new TextEncoder()
1142 return yield* batched(self, {
1143 window: options?.batchWindow ?? 1000,
1144 flush: (output) => effect.ignore(logFile.writeAll(encoder.encode(output.join("\n") + "\n")))
1145 })
1146 })
1147)