已发布 上游基线 400740a 原文 ↗ 在 GitHub 编辑

日志

了解 Effect 的日志工具,包括动态日志级别、自定义输出以及对日志的细粒度控制。

日志是软件开发中的一个重要方面,尤其是在调试和监控应用程序行为时。在本节中,我们将探索 Effect 的日志工具,并看看它们与传统日志记录方法有何不同。

相比传统日志记录的优势

相比传统的日志记录方式,Effect 的日志工具带来了几项优势:

  1. 动态日志级别控制:借助 Effect 的日志功能,你可以动态更改日志级别。这意味着你能根据严重程度控制哪些日志消息会被展示。例如,你可以把应用配置为只记录警告或错误,这在生产环境中对降低噪音非常有帮助。

  2. 自定义日志输出:Effect 的日志工具允许你改变日志的处理方式。借助自定义 logger,你可以把日志消息导向各种目的地,例如某个服务或某个文件。这种灵活性确保日志的存储与处理方式最贴合你的应用需求。

  3. 细粒度日志:Effect 支持按程序的各个部分对日志进行细粒度控制。你可以为应用的不同部分设置不同的日志级别,从而为每个具体组件定制详细程度。这在调试和排查问题时非常有价值,因为你可以专注于最重要的信息。

  4. 基于环境的日志:Effect 的日志工具可以与部署环境结合,实现精细的日志策略。例如,在开发期间,你可能会选择以 trace 级别及以上记录所有内容,以便详细调试。相比之下,生产版本可以配置为只记录错误或严重问题,从而把对性能的影响以及生产日志中的噪音降到最低。

  5. 其他特性:Effect 的日志工具还带有其他特性,例如测量时间跨度、按 effect 调整日志级别,以及集成 span 用于性能监控。

log

Effect.log 函数允许你以默认的 INFO 级别记录一条消息。

示例(记录一条简单消息)

import { Effect, Logger } from "effect"

const program = Effect.log("Application started")

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Application started"
*/

// Capture the logged message content (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["Application started"]]

Effect 中的默认 logger 会为每条日志条目添加若干有用的细节:

注解说明
timestamp日志消息生成时的时间戳。
level记录该消息时使用的日志级别(例如 INFOERROR)。
fiber执行该程序的 fiber 的标识符。
message日志消息的内容,可以包含多个字符串或值。
span(可选)span 的持续时间,单位为毫秒,可帮助你了解各项操作的耗时。
Customizing Loggers

关于如何定制日志设置以满足具体需求(例如集成自定义日志框架或 调整日志格式),请参阅 自定义 logger 一节。

你也可以一次记录多条消息。

示例(记录多条消息)

import { Effect, Logger } from "effect"

const program = Effect.log("message1", "message2", "message3")

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 message=message2 message=message3
*/

// Capture the logged message content (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["message1", "message2", "message3"]]

为了提供更多上下文,你还可以在日志中包含一个或多个 Cause 实例, 它们会在额外的 cause 注解下提供详细的错误信息:

示例(记录带 cause 的日志)

import { Effect, Cause, Logger } 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!"
*/

// Capture the logged message content and cause (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
const causes: Array<Cause.Cause<unknown>> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          messages.push(options.message)
          causes.push(options.cause)
        }),
      ]),
    ),
  ),
)
messages // => [["message1", "message2"]]
Cause.hasDies(causes[0]) // => true

日志级别

logDebug

默认情况下,DEBUG 消息不会被展示。要为某个 effect 启用它们,请提供值为 "Debug"References.MinimumLogLevel 上下文引用。

示例(启用调试日志)

import { Effect, References, Logger } from "effect"

const task1 = Effect.gen(function* () {
  yield* Effect.sleep("2 seconds")
  yield* Effect.logDebug("task1 done") // Log a debug message
}).pipe(Effect.provideService(References.MinimumLogLevel, "Debug")) // Enable DEBUG level

const task2 = Effect.gen(function* () {
  yield* Effect.sleep("1 second")
  yield* Effect.logDebug("task2 done") // This message won't be logged
})

const program = Effect.gen(function* () {
  yield* Effect.log("start")
  yield* task1
  yield* task2
  yield* Effect.log("done")
})

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO message=start
timestamp=... level=DEBUG message="task1 done" <-- 2 seconds later
timestamp=... level=INFO message=done <-- 1 second later
*/

// Capture the logged messages (ignoring the non-deterministic timestamps)
const messages: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
// task2's debug message is filtered out, since it never enables the Debug level
messages // => [["start"], ["task1 done"], ["done"]]
Controlling Log Levels Per Effect

只向相关的 effect 提供 References.MinimumLogLevel,就能在不改变 周围程序的情况下控制它的日志输出。

logInfo

INFO 日志级别默认会被展示。该级别通常用于一般性的应用事件或进度更新。

示例(以 INFO 级别记录日志)

import { Effect, Logger } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.logInfo("start")
  yield* Effect.sleep("2 seconds")
  yield* Effect.sleep("1 second")
  yield* Effect.logInfo("done")
})

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO message=start
timestamp=... level=INFO message=done <-- 3 seconds later
*/

// Capture the logged messages (ignoring the non-deterministic timestamps)
const messages: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["start"], ["done"]]

logWarning

WARN 日志级别默认会被展示。该级别用于那些不会立即打断程序流程、但应当被关注的潜在问题或警告。

示例(以 WARN 级别记录日志)

import { Effect, Result, Logger } from "effect"

const task = Effect.fail("Oh uh!").pipe(Effect.as(2))

const program = Effect.gen(function* () {
  const failureOrSuccess = yield* Effect.result(task)
  if (Result.isFailure(failureOrSuccess)) {
    yield* Effect.logWarning(failureOrSuccess.failure)
    return 0
  } else {
    return failureOrSuccess.success
  }
})

Effect.runFork(program)
/*
Output:
timestamp=... level=WARN fiber=#0 message="Oh uh!"
*/

// Capture the logged message content (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
const result = await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
result // => 0
messages // => [["Oh uh!"]]

logError

ERROR 日志级别默认会被展示。这些消息表示需要处理的问题。

示例(以 ERROR 级别记录日志)

import { Effect, Result, Logger } from "effect"

const task = Effect.fail("Oh uh!").pipe(Effect.as(2))

const program = Effect.gen(function* () {
  const failureOrSuccess = yield* Effect.result(task)
  if (Result.isFailure(failureOrSuccess)) {
    yield* Effect.logError(failureOrSuccess.failure)
    return 0
  } else {
    return failureOrSuccess.success
  }
})

Effect.runFork(program)
/*
Output:
timestamp=... level=ERROR fiber=#0 message="Oh uh!"
*/

// Capture the logged message content (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
const result = await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
result // => 0
messages // => [["Oh uh!"]]

logFatal

FATAL 日志级别默认会被展示。该日志级别通常保留给不可恢复的错误。

示例(以 FATAL 级别记录日志)

import { Effect, Result, Logger } from "effect"

const task = Effect.fail("Oh uh!").pipe(Effect.as(2))

const program = Effect.gen(function* () {
  const failureOrSuccess = yield* Effect.result(task)
  if (Result.isFailure(failureOrSuccess)) {
    yield* Effect.logFatal(failureOrSuccess.failure)
    return 0
  } else {
    return failureOrSuccess.success
  }
})

Effect.runFork(program)
/*
Output:
timestamp=... level=FATAL fiber=#0 message="Oh uh!"
*/

// Capture the logged message content (ignoring the non-deterministic timestamp)
const messages: Array<unknown> = []
const result = await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
result // => 0
messages // => [["Oh uh!"]]

自定义注解

你可以使用 Effect.annotateLogs 函数添加自定义注解,从而增强日志输出。 这样可以让你为每条日志条目附加额外的元数据,提升可追溯性并提供更多上下文。

添加单个注解

你可以以键/值对的形式,把单个注解应用到某个 effect 内的所有日志条目上。

示例(单个键/值注解)

import { Effect, Logger, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("message1")
  yield* Effect.log("message2")
}).pipe(
  // Annotation as key/value pair
  Effect.annotateLogs("key", "value"),
)

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 key=value
timestamp=... level=INFO fiber=#0 message=message2 key=value
*/

// Capture the annotations attached to each logged message
const annotations: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) =>
          annotations.push(
            options.fiber.getRef(References.CurrentLogAnnotations),
          ),
        ),
      ]),
    ),
  ),
)
annotations // => [{ key: "value" }, { key: "value" }]

在这个例子中,program 内生成的所有日志都会包含注解 key=value

Scope of Annotations

通过 Effect.annotateLogs 应用的注解会自动添加到被注解 effect 作用域内生成的 所有日志中,包括来自嵌套 effect 的日志。

嵌套 effect 中的注解

注解会传播到嵌套 effect 或下游 effect 中生成的所有日志,从而确保任何子 effect 的日志都继承父 effect 的注解。

示例(把注解传播到嵌套 effect)

在这个例子中,注解 key=value 会出现在所有日志中,甚至包括来自嵌套 anotherProgram effect 的日志。

import { Effect, Logger } from "effect"

// Define a child program that logs an error
const anotherProgram = Effect.gen(function* () {
  yield* Effect.logError("error1")
})

// Define the main program
const program = Effect.gen(function* () {
  yield* Effect.log("message1")
  yield* Effect.log("message2")
  yield* anotherProgram // Call the nested program
}).pipe(
  // Attach an annotation to all logs in the scope
  Effect.annotateLogs("key", "value"),
)

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 key=value
timestamp=... level=INFO fiber=#0 message=message2 key=value
timestamp=... level=ERROR fiber=#0 message=error1 key=value
*/

// Capture the logged messages, confirming the annotation reaches the nested effect too
const messages: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["message1"], ["message2"], ["error1"]]

添加多个注解

你也可以通过传入一个包含键/值对的对象,一次应用多个注解。每一对键/值都会被添加到该 effect 内的每一条日志记录中。

示例(多个注解)

import { Effect, Logger, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("message1")
  yield* Effect.log("message2")
}).pipe(
  // Add multiple annotations
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
)

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message=message1 key2=value2 key1=value1
timestamp=... level=INFO fiber=#0 message=message2 key2=value2 key1=value1
*/

// Capture the annotations attached to each logged message
const annotations: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) =>
          annotations.push(
            options.fiber.getRef(References.CurrentLogAnnotations),
          ),
        ),
      ]),
    ),
  ),
)
annotations // => [{ key1: "value1", key2: "value2" }, { key1: "value1", key2: "value2" }]

在这种情况下,每条日志都会同时包含 key1=value1key2=value2

作用域内的注解

如果你希望限制注解的作用范围,使它们只对特定的日志记录生效,可以使用 Effect.annotateLogsScoped。这个函数会把注解限制在特定作用域内产生的日志上。

示例(作用域内的注解)

import { Effect, Logger, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("no annotations") // No annotations
  yield* Effect.annotateLogsScoped({ key: "value" }) // Scoped annotation
  yield* Effect.log("message1") // Annotation applied
  yield* Effect.log("message2") // Annotation applied
}).pipe(
  Effect.scoped,
  // Outside scope, no annotations
  Effect.andThen(Effect.log("no annotations again")),
)

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="no annotations"
timestamp=... level=INFO fiber=#0 message=message1 key=value
timestamp=... level=INFO fiber=#0 message=message2 key=value
timestamp=... level=INFO fiber=#0 message="no annotations again"
*/

// Capture each logged message together with the annotations active at that point
const records: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          records.push({
            message: options.message,
            annotations: options.fiber.getRef(References.CurrentLogAnnotations),
          })
        }),
      ]),
    ),
  ),
)
records // => [{ message: ["no annotations"], annotations: {} }, { message: ["message1"], annotations: { key: "value" } }, { message: ["message2"], annotations: { key: "value" } }, { message: ["no annotations again"], annotations: {} }]

日志 Span

Effect 内置支持日志 span(log span),它可以让你测量并记录特定任务或代码片段的耗时。这个特性有助于追踪某些操作耗费了多长时间,让你对应用的性能有更深入的了解。

示例(用日志 Span 测量任务耗时)

import { Effect, Logger, References } from "effect"

const program = Effect.gen(function* () {
  // Simulate a delay to represent a task taking time
  yield* Effect.sleep("1 second")
  // Log a message indicating the job is done
  yield* Effect.log("The job is finished!")
}).pipe(
  // Apply a log span labeled "myspan" to measure
  // the duration of this operation
  Effect.withLogSpan("myspan"),
)

Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
*/

// Capture the logged message and the active span label (the duration itself is non-deterministic)
const records: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          records.push({
            message: options.message,
            spans: options.fiber
              .getRef(References.CurrentLogSpans)
              .map(([label]) => label),
          })
        }),
      ]),
    ),
  ),
)
records // => [{ message: ["The job is finished!"], spans: ["myspan"] }]

禁用默认日志

有时,比如在测试执行期间,你可能希望禁用应用中的默认日志。Effect 提供了几种在需要时关闭日志的方式。本节中,我们来看看在 Effect 框架中禁用日志的不同方法。

示例(提供最低日志级别)

有一种便捷的禁用日志方式:提供 References.MinimumLogLevel,并把它的值设为 "None"

import { Effect, Logger, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("Executing task...")
  yield* Effect.sleep("100 millis")
  console.log("task done")
})

// Default behavior: logging enabled
Effect.runFork(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Executing task..."
task done
*/

// Disable logging by setting minimum log level to 'None'
Effect.runFork(
  program.pipe(Effect.provideService(References.MinimumLogLevel, "None")),
)
/*
Output:
task done
*/

// Confirm that the minimum log level actually suppresses the log message
const enabledMessages: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => enabledMessages.push(options.message)),
      ]),
    ),
  ),
)
enabledMessages.length // => 1

const disabledMessages: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provideService(References.MinimumLogLevel, "None"),
    Effect.provide(
      Logger.layer([
        Logger.make((options) => disabledMessages.push(options.message)),
      ]),
    ),
  ),
)
disabledMessages.length // => 0

示例(使用 Layer)

另一种禁用日志的方式是创建一个将最低日志级别设为 "None" 的 Layer,这样就可以彻底关闭所有日志输出。

import { Effect, Layer, Logger, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("Executing task...")
  yield* Effect.sleep("100 millis")
  console.log("task done")
})

// Create a layer that disables logging
const layer = Layer.succeed(References.MinimumLogLevel, "None")

// Apply the layer to disable logging
Effect.runFork(program.pipe(Effect.provide(layer)))
/*
Output:
task done
*/

// Confirm that no log message was emitted
const messages: Array<unknown> = []
await Effect.runPromise(
  program.pipe(
    Effect.provide(layer),
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages.length // => 0

示例(使用自定义 Runtime)

你也可以通过创建一个包含关闭日志配置的自定义 Runtime 来禁用日志:

import { Effect, Layer, Logger, ManagedRuntime, References } from "effect"

const program = Effect.gen(function* () {
  yield* Effect.log("Executing task...")
  yield* Effect.sleep("100 millis")
  console.log("task done")
})

// Create a custom runtime that disables logging
const customRuntime = ManagedRuntime.make(
  Layer.succeed(References.MinimumLogLevel, "None"),
)

// Run the program using the custom runtime
customRuntime.runFork(program)
/*
Output:
task done
*/

// Confirm that no log message was emitted through a runtime with logging disabled
const messages: Array<unknown> = []
const capturingRuntime = ManagedRuntime.make(
  Layer.merge(
    Layer.succeed(References.MinimumLogLevel, "None"),
    Logger.layer([Logger.make((options) => messages.push(options.message))]),
  ),
)
await capturingRuntime.runPromise(program)
messages.length // => 0

从配置中加载日志级别

若要从配置中加载日志级别并应用到你的程序,请把配置值映射为一个用于 References.MinimumLogLevel 的 Layer。

示例(从配置中加载日志级别)

import {
  Effect,
  Config,
  Layer,
  ConfigProvider,
  References,
  Logger,
} from "effect"

// Simulate a program with logs
const program = Effect.gen(function* () {
  yield* Effect.logError("ERROR!")
  yield* Effect.logWarning("WARNING!")
  yield* Effect.logInfo("INFO!")
  yield* Effect.logDebug("DEBUG!")
})

// Load the log level from the configuration and apply it as a layer
const LogLevelLive = Config.LogLevel("LOG_LEVEL").pipe(
  Effect.map((level) =>
    // Set the minimum log level
    Layer.succeed(References.MinimumLogLevel, level),
  ),
  Layer.unwrap, // Convert the effect into a layer
)

// Provide the loaded log level to the program
const configured = Effect.provide(program, LogLevelLive)

// Test the program using a mock configuration provider
const test = Effect.provide(
  configured,
  ConfigProvider.layer(ConfigProvider.fromUnknown({ LOG_LEVEL: "Warn" })),
)

Effect.runFork(test)
/*
Output:
... level=ERROR fiber=#0 message=ERROR!
... level=WARN fiber=#0 message=WARNING!
*/

// Capture which messages actually pass the configured "Warn" minimum level
const messages: Array<unknown> = []
await Effect.runPromise(
  test.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["ERROR!"], ["WARNING!"]]
Using ConfigProvider for Testing

ConfigProvider.fromUnknown 函数可以模拟配置值,因此很适合用于测试。 示例见加载内存中的对象

自定义 Logger

本节中,你将学习如何定义自定义 logger 并把它设为应用中的默认 logger。自定义 logger 让你可以控制日志消息的处理方式,例如把它们路由到外部服务、写入文件,或以特定方式格式化日志。

定义自定义 Logger

你可以使用 Logger.make 函数定义自己的 logger。这个函数允许你指定日志消息应当如何处理。

示例(定义一个简单的自定义 Logger)

import { Logger } from "effect"

// Custom logger that outputs log messages to the console
const logger = Logger.make(({ logLevel, message }) => {
  globalThis.console.log(`[${logLevel.toUpperCase()}] ${message}`)
})

Logger.isLogger(logger) // => true

在这个例子中,自定义 logger 把日志以 [LogLevel] Message 的格式连同日志级别和消息一起输出到控制台。

在程序中使用自定义 Logger

假设你已有下面这些任务,以及一个记录若干消息的程序:

import { Effect, Logger } from "effect"

// Custom logger that outputs log messages to the console
const logger = Logger.make(({ logLevel, message }) => {
  globalThis.console.log(`[${logLevel.toUpperCase()}] ${message}`)
})

const task1 = Effect.gen(function* () {
  yield* Effect.sleep("2 seconds")
  yield* Effect.logDebug("task1 done")
})

const task2 = Effect.gen(function* () {
  yield* Effect.sleep("1 second")
  yield* Effect.logDebug("task2 done")
})

const program = Effect.gen(function* () {
  yield* Effect.log("start")
  yield* task1
  yield* task2
  yield* Effect.log("done")
})

Effect.isEffect(program) // => true

创建一个 Logger.layer,其中包含应当接收消息的 logger,然后用 Effect.provide 把它提供给程序。

示例(用自定义 Logger 替换默认 Logger)

import { Effect, Logger, References } from "effect"

// Custom logger that outputs log messages to the console
const logger = Logger.make(({ logLevel, message }) => {
  globalThis.console.log(`[${logLevel.toUpperCase()}] ${message}`)
})

const task1 = Effect.gen(function* () {
  yield* Effect.sleep("2 seconds")
  yield* Effect.logDebug("task1 done")
})

const task2 = Effect.gen(function* () {
  yield* Effect.sleep("1 second")
  yield* Effect.logDebug("task2 done")
})

const program = Effect.gen(function* () {
  yield* Effect.log("start")
  yield* task1
  yield* task2
  yield* Effect.log("done")
})

// Replace the default logger with the custom logger
const layer = Logger.layer([logger, Logger.tracerLogger])

Effect.runFork(
  program.pipe(
    Effect.provideService(References.MinimumLogLevel, "Debug"),
    Effect.provide(layer),
  ),
)

// Capture the level+message pairs delivered to the custom logger
const entries: Array<string> = []
await Effect.runPromise(
  program.pipe(
    Effect.provideService(References.MinimumLogLevel, "Debug"),
    Effect.provide(
      Logger.layer([
        Logger.make(({ logLevel, message }) => {
          entries.push(`[${logLevel.toUpperCase()}] ${message}`)
        }),
      ]),
    ),
  ),
)
entries // => ["[INFO] start", "[DEBUG] task1 done", "[DEBUG] task2 done", "[INFO] done"]

运行上面的程序时,控制台会打印如下日志消息:

[INFO] start
[DEBUG] task1 done
[DEBUG] task2 done
[INFO] done

内置 Logger

Effect 提供了若干内置 logger,你可以根据自己的日志记录需求选用。这些 logger 提供不同的格式,各自适用于不同的环境或用途,例如开发、生产,或与外部日志服务集成。

每个 logger 都以两种形式提供:logger 本身,以及一个使用该 logger 并把输出发送到 Console 默认服务 的 layer。例如,structuredLogger logger 以详细的对象格式生成日志,而 structured layer 使用同一个 logger,并把输出写入 Console 服务。

stringLogger(默认)

stringLogger logger 以人类可读的键值风格生成日志。这种格式在开发和生产中都很常用,因为它简单,并且易于在控制台中阅读。

由于它是默认 logger,因此这个 logger 没有对应的 layer。

import { Effect, Logger } from "effect"

const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe(
  Effect.delay("100 millis"),
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
  Effect.withLogSpan("myspan"),
)

Effect.runFork(program)

// Capture the formatted log line (ignoring the non-deterministic timestamp/fiber/span duration)
let formatted = ""
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          formatted = Logger.formatSimple.log(options)
        }),
      ]),
    ),
  ),
)
formatted.includes("message=msg1 message=msg2") // => true
formatted.endsWith("key1=value1 key2=value2") // => true

输出:

timestamp=2024-12-28T10:44:31.281Z level=INFO fiber=#0 message=msg1 message=msg2 message="[
  \"msg3\",
  \"msg4\"
]" myspan=102ms key2=value2 key1=value1

logfmtLogger

logfmtLogger logger 以人类可读的键值格式生成日志,与 stringLogger logger 类似。主要区别在于,logfmtLogger 会移除多余的空格,让日志更紧凑。

import { Effect, Logger } from "effect"

const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe(
  Effect.delay("100 millis"),
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
  Effect.withLogSpan("myspan"),
)

Effect.runFork(
  program.pipe(
    Effect.provide(Logger.layer([Logger.consoleLogFmt, Logger.tracerLogger])),
  ),
)

// Capture the formatted log line (ignoring the non-deterministic timestamp/fiber/span duration)
let formatted = ""
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          formatted = Logger.formatLogFmt.log(options)
        }),
      ]),
    ),
  ),
)
formatted.includes("message=msg1 message=msg2") // => true
formatted.endsWith("key1=value1 key2=value2") // => true

输出:

timestamp=2024-12-28T10:44:31.281Z level=INFO fiber=#0 message=msg1 message=msg2 message="[\"msg3\",\"msg4\"]" myspan=102ms key2=value2 key1=value1

prettyLogger

prettyLogger logger 通过颜色和缩进来增强日志输出,以获得更好的可读性,因此在开发阶段需要目视浏览控制台日志时尤其有用。

import { Effect, Logger } from "effect"

const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe(
  Effect.delay("100 millis"),
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
  Effect.withLogSpan("myspan"),
)

Effect.runFork(
  program.pipe(
    Effect.provide(Logger.layer([Logger.consolePretty(), Logger.tracerLogger])),
  ),
)

// Wait for the fire-and-forget run above to finish logging before capturing below
await Effect.runPromise(Effect.sleep("150 millis"))

// Capture the individual console lines written by the pretty logger
// (ignoring the non-deterministic timestamp/fiber/span duration on the first line)
const calls: Array<Array<unknown>> = []
const originalLog = console.log
console.log = (...args: Array<unknown>) => {
  calls.push(args)
}
try {
  await Effect.runPromise(
    program.pipe(
      Effect.provide(
        Logger.layer([Logger.consolePretty(), Logger.tracerLogger]),
      ),
    ),
  )
} finally {
  console.log = originalLog
}
calls.length // => 5
calls.slice(1) // => [["msg2"], [["msg3", "msg4"]], ["key1:", "value1"], ["key2:", "value2"]]

输出:

[11:37:14.265] INFO (#0) myspan=101ms: msg1
  msg2
  [ 'msg3', 'msg4' ]
  key2: value2
  key1: value1

structuredLogger

structuredLogger logger 以详细的对象格式生成日志。当你需要更可追溯的日志时,这种格式很有帮助,特别是当其他系统要分析这些日志、或将其存储起来以便日后查看时。

import { Effect, Logger } from "effect"

const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe(
  Effect.delay("100 millis"),
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
  Effect.withLogSpan("myspan"),
)

Effect.runFork(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.consoleStructured, Logger.tracerLogger]),
    ),
  ),
)

// Capture the structured record (ignoring the non-deterministic timestamp/fiberId/span duration)
let structured: any
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          structured = Logger.formatStructured.log(options)
        }),
      ]),
    ),
  ),
)
structured.message // => ["msg1", "msg2", ["msg3", "msg4"]]
structured.level // => "INFO"
structured.annotations // => { key1: "value1", key2: "value2" }
Object.keys(structured.spans) // => ["myspan"]

输出:

{
  message: [ 'msg1', 'msg2', [ 'msg3', 'msg4' ] ],
  level: 'INFO',
  timestamp: '2024-12-28T10:44:31.281Z',
  cause: undefined,
  annotations: { key2: 'value2', key1: 'value1' },
  spans: { myspan: 102 },
  fiberId: '#0'
}
字段说明
message经过处理的单个值,或经过处理的值组成的数组,取决于记录了多少条消息。
level表示日志级别标签的字符串(例如 “INFO” 或 “DEBUG”)。
timestamp日志生成时刻的 ISO 8601 时间戳(例如 “2024-01-01T00:00:00.000Z”)。
cause展示详细错误信息的字符串;如果未提供 cause,则为 undefined
annotations一个对象,其中每个键是一个注解标签,对应的值会被解析为结构化格式(例如 {"key": "value"})。
spans一个对象,把每个 span 标签映射到它的毫秒级时长,该时长从 span 开始计时算起,到调用 logger 的那一刻为止(例如 {"myspan": 102})。
fiberId生成这条日志的 fiber 的标识符(例如 “#0”)。

jsonLogger

jsonLogger logger 以 JSON 格式生成日志。对于需要解析并存储 JSON 日志的工具或服务来说,这很有用。 它会对 structuredLogger logger 创建的对象调用 JSON.stringify

import { Effect, Logger } from "effect"

const program = Effect.log("msg1", "msg2", ["msg3", "msg4"]).pipe(
  Effect.delay("100 millis"),
  Effect.annotateLogs({ key1: "value1", key2: "value2" }),
  Effect.withLogSpan("myspan"),
)

Effect.runFork(
  program.pipe(
    Effect.provide(Logger.layer([Logger.consoleJson, Logger.tracerLogger])),
  ),
)

// Capture the JSON record (ignoring the non-deterministic timestamp/fiberId/span duration)
let jsonString = ""
await Effect.runPromise(
  program.pipe(
    Effect.provide(
      Logger.layer([
        Logger.make((options) => {
          jsonString = Logger.formatJson.log(options)
        }),
      ]),
    ),
  ),
)
const parsed = JSON.parse(jsonString)
parsed.message // => ["msg1", "msg2", ["msg3", "msg4"]]
parsed.level // => "INFO"
parsed.annotations // => { key1: "value1", key2: "value2" }
Object.keys(parsed.spans) // => ["myspan"]

输出:

{"message":["msg1","msg2",["msg3","msg4"]],"level":"INFO","timestamp":"2024-12-28T10:44:31.281Z","annotations":{"key2":"value2","key1":"value1"},"spans":{"myspan":102},"fiberId":"#0"}

组合多个 Logger

转发到多个 Logger

自定义 logger 可以调用其他 logger,从而把每条消息都转发给它们。

示例(组合两个 Logger)

import { Effect, Logger } from "effect"

// Define a custom logger that logs to the console
const logger = Logger.make(({ logLevel, message }) => {
  globalThis.console.log(`[${logLevel.toUpperCase()}] ${message}`)
})

// Combine the default logger and the custom logger
//
//      ┌─── Logger<unknown, [void, void]>
//      ▼
const combined = Logger.make((options) => [
  Logger.defaultLogger.log(options),
  logger.log(options),
])

const program = Effect.log("something")

Effect.runFork(
  program.pipe(
    // Replace the default logger with the combined logger
    Effect.provide(Logger.layer([combined, Logger.tracerLogger])),
  ),
)
/*
Output:
timestamp=2025-01-09T13:50:58.655Z level=INFO fiber=#0 message=something
[INFO] something
*/

// Capture the message that reaches the combined logger
const messages: Array<unknown> = []
Effect.runSync(
  program.pipe(
    Effect.provide(
      Logger.layer([Logger.make((options) => messages.push(options.message))]),
    ),
  ),
)
messages // => [["something"]]