@effect/opentelemetry 日志修复实录:Effect LogLevel 与 OTel SeverityNumber 的规范映射
2026/9/14 3:50:01 网站建设 项目流程

@effect/opentelemetry 日志修复实录:Effect LogLevel 与 OTel SeverityNumber 的规范映射

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

@effect/opentelemetry是 Effect 生态中把 Effect 日志、追踪与指标导出到 OpenTelemetry SDK 的集成包(本仓库对应的源码位于 .repos/effect-smol/packages/opentelemetry)。本文基于该仓库的变更记录 fix-otel-logger-severity-number.md,讲解其中一项关键修复:OtelLogger.make此前把 Effect 内部日志级别序号(如Info=20000)直接当作 OpenTelemetry 日志的severityNumber字段上报,导致该字段超出 OTel 日志数据模型规定的1~24范围,被 Honeycomb、Datadog 等严格校验的后端归类为UNSPECIFIED。修复后二者按规范逐级映射,并导出了可复用的辅助函数logLevelToSeverityNumber。读完本文,你将理解 OTel SeverityNumber 规范、该 Bug 的根因与修复路径,以及如何在自己的 Effect 应用中正确接入 OTel 日志。

背景:Effect 日志如何接入 OpenTelemetry Logs

在了解修复之前,先明确这条链路。OtelLogger.ts模块的职责是把 Effect 的日志事件(log event)转换为 OpenTelemetry 的日志记录(log record),其核心构件如下(见 OtelLogger.ts):

  • OtelLoggerProvider:一个Context.Service,内部持有 OpenTelemetry 的LoggerProvider,是日志导出的出口;
  • make:工厂函数,从OtelLoggerProviderClock.Clock构建出一个 Effect 的Logger,任何 Effect 日志经它 emit 到 OTel;
  • layer:安装层,把make创建的 logger 合并(mergeWithExisting默认为true)进当前应用;设为false则替换现有 logger 集合;
  • layerLoggerProvider:作用域化的 provider 层,接收一个或多个LogRecordProcessor,配合当前Resource创建Otel.LoggerProvider,并在层释放时先forceFlushshutdown(超时默认 3 秒)。

整个模块通过 index.ts 以OtelLogger命名空间导出。安装方式见 opentelemetry/README.md:

npm install effect@rc @effect/opentelemetry@rc

其中@opentelemetry/*系列 SDK 包按需作为 peer dependency 提供。

问题根因:把内部序号当成了规范值

变更记录明确指出修复前的问题:OtelLogger.make使用LogLevel.getOrdinal(level)作为severityNumber上报。而 Effect 的getOrdinal返回的是内部排序序号——在 LogLevel.ts 中定义,Trace对应10000Info对应20000Warn对应30000Error对应40000Fatal对应50000,粒度是 10000 的倍数。

问题在于:OpenTelemetry 日志数据模型(Logs Data Model)规定的SeverityNumber合法范围是1 到 24getOrdinal产出的 20000、40000 这类数值远远超出该区间,属于无效值。对于 Honeycomb、Datadog 等会对该字段做合法性校验的后端,这类值会被统一归并为UNSPECIFIED(即 0),使得日志的严重级别在传输与存储过程中被抹平,无法按级别过滤与告警

修复方案:按 OTel 规范逐级映射

修复的核心是彻底替换取值来源,不再使用 Effect 内部序号,而是建立一张规范化的映射表,将六个 Effect 日志级别逐一对应到 OTel 规范规定的 SeverityNumber。变更记录给出了完整映射:

Effect LogLevelOTel SeverityNumber
TraceTRACE (1)
DebugDEBUG (5)
InfoINFO (9)
WarnWARN (13)
ErrorERROR (17)
FatalFATAL (21)

这张表遵循 OTel 规范中“每个级别对应间隔 4 的奇数数值”的设计(TRACE=1、DEBUG=5、INFO=9、WARN=13、ERROR=17、FATAL=21),预留了偶数位供将来细分级别使用,例如 INFO 与 WARN 之间可插入 10~12 的中间值。因此映射后上报的日志在 Honeycomb、Datadog 等支持 OTel 日志语义的后端中可以正确展示级别、按级别过滤,并触发对应的告警策略。

源码级验证:logLevelToSeverityNumber的实现

在 OtelLogger.ts 中可以看到该辅助函数的具体实现:

export const logLevelToSeverityNumber = (level: LogLevel.LogLevel): SeverityNumber => { switch (level) { case "Trace": return SeverityNumber.TRACE case "Debug": return SeverityNumber.DEBUG case "Info": return SeverityNumber.INFO case "Warn": return SeverityNumber.WARN case "Error": return SeverityNumber.ERROR case "Fatal": return SeverityNumber.FATAL default: return SeverityNumber.UNSPECIFIED } }

几个值得注意的实现细节:

  • 函数通过穷举switch完成六个级别的显式映射,default分支兜底返回SeverityNumber.UNSPECIFIED,保证类型层面未预期的新级别不会抛出异常;
  • 返回值类型直接使用@opentelemetry/api-logsSeverityNumber枚举,语义清晰且与 OTel SDK 完全对齐;
  • 该函数标注为@category converting@since 4.0.0,作为公开 API 从模块导出,供下游复用到任意需要“Effect 级别 → OTel 级别”转换的场景。

make中,emit 日志记录时正是用该函数填充severityNumber字段(见 OtelLogger.ts):

otelLogger.emit({ body: message.length === 1 ? message[0] : message, severityText: options.logLevel, severityNumber: logLevelToSeverityNumber(options.logLevel), timestamp: hrTime, observedTimestamp: hrTime, attributes })

可以看到severityText仍保留 Effect 的原始级别字符串(如"Info"),与数值型的severityNumber互为补充,符合 OTel 规范对二者关系的要求;severityText是给人看的文本,severityNumber是给系统做过滤和排序的数值。

测试保障:六级别映射被逐项断言

该修复并非无凭据的改动,仓库中的测试 OtelLogger.test.ts 通过InMemoryLogRecordExporter对映射做了端到端验证:依次执行Effect.logTracelogDebuglogInfologWarninglogErrorlogFatal,随后把导出记录按severityText建索引,逐项断言其severityNumberSeverityNumber枚举值严格相等。

值得注意,该测试通过Layer.succeed(References.MinimumLogLevel, "Trace")把最小日志级别降到Trace,确保六条日志全部实际产生,再对byText.Trace === SeverityNumber.TRACEbyText.Error === SeverityNumber.ERROR等断言做严格校验。这从测试层面固化了修复后的行为,防止后续改动把映射回退成getOrdinal的实现。

应用示例:在 Effect 应用中启用 OTel 日志

结合上述 API,一个完整启用 OTel 日志的最小结构如下(参考 OtelLogger.test.ts 中NodeSdk.layer的用法):

import * as NodeSdk from "@effect/opentelemetry/NodeSdk" import { SimpleLogRecordProcessor, InMemoryLogRecordExporter } from "@opentelemetry/sdk-logs" const LoggingLayer = NodeSdk.layer(() => ({ resource: { serviceName: "my-service" }, logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: new InMemoryLogRecordExporter() })] })) // 在程序入口 Effect.provide(LoggingLayer) 后,Effect.log 系列调用 // 会自动以规范化的 SeverityNumber 上报到 OTel

在实际项目中,把InMemoryLogRecordExporter替换为OtlpLogRecordExporter等生产级导出器即可把日志送往 Collector、Honeycomb 或 Datadog 等后端;而借助logLevelToSeverityNumber这一公开导出,自定义的日志适配器也可以在 OTel 生态外复用同一套规范的级别换算逻辑。

关联变更与演进方向

该修复属于@effect/opentelemetrypatch级变更(见 .changeset/pre/fix-otel-logger-severity-number.md 的 frontmatter),不破坏既有 API 签名,仅修正数值语义并新增导出。同目录下还并存着多条 OTel 日志相关的预发布变更,例如 explicit-otel-service-identity.md、otel-resource-env-precedence.md、fix-otel-logger-clock-skew.md、fix-otel-logger-shutdown.md 与 preserve-otel-parent-context.md,它们共同勾勒出@effect/opentelemetry日志链路在服务标识、环境变量优先级、时钟对齐、优雅关闭与上下文保持等方面的持续完善方向。

小结

fix-otel-logger-severity-number这项变更以一张规范映射表解决了 Effect 日志接入 OpenTelemetry 时最容易被忽视的“数值语义”问题:内部排序序号与行业标准数值是两个不同的概念,前者服务于框架自身的比较逻辑,后者必须严格遵循 OTel 日志数据模型。修复通过新增logLevelToSeverityNumber辅助函数、在make中替换取值来源、并配以逐级别断言的测试,让 Effect 应用导出的日志在 Honeycomb、Datadog 等后端中能够被正确识别级别、过滤与告警——这也是任何把 Effect 日志接入 OTel 生态的应用都应当遵循的基线行为。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询