把日志接进 OpenTelemetry,看起来像一道映射题。消息变成 Body,级别映射到 Severity,普通字段变成 Attributes,合法的关联字段可以变成 trace context。然后桥接器调用 Emit。这部分是直截了当的。困难的部分是保住 API 之间那些看不见的契约:用来询问某条记录是否被启用的上下文、池化条目归还之后的所有权、一个字段有不止一种存储形式时的掩码优先级,以及桥接器并不拥有的 provider 的生命周期。 这些不是桥接器周边的细节。它们决定桥接器是发出正确的记录,还是泄漏一个本该被掩码的值,是保留了不属于自己的内存,还是在进程退出前丢掉最后一条日志。这篇文章讲的就是这些边界。 **三条桥接契约** 契约一:Enabled 和 Emit 必须使用同一个关联上下文。OpenTelemetry 暴露 Logger.Enabled,是为了让桥接器在管道不想要某条记录时跳过昂贵的记录构造。这个方法接收的上下文,就是将要关联到记录上的那个上下文。用 context.Background() 去便宜地探测、构造记录、重建 span context,再用更丰富的上下文调用 Emit,看起来省事,但处理器可能根据采样标志或上下文里的其他值做过滤。这样一来,桥接器就在一个上下文下测试准入,在另一个上下文下发送。 契约二:发出的字节拥有自己的存储。池化条目归还之后,所有权归谁,必须说清楚。 契约三:flush 要针对正在发送的 provider。桥接器并不拥有这个 provider 的生命周期。 **测量边界也要说清楚** 每个计时的操作都直接在计时器之前构造好的 LogEntry 上调用 Adapter.Write,终止于一个 Emit 方法为空操作的 OpenTelemetry API Logger。这些数字不包含 HaloLog 的公开 logger 和 builder 路径、掩码、控制台/文件扇出、OpenTelemetry SDK、处理器、导出器、收集器、网络和后端。它们是适配器微基准,不是生产吞吐量,也不是与其他桥接器的对比。 **需求比 trace ID 更大** HaloLog 原本已经有一个可选的 Bind 辅助方法,能把 trace_id、span_id 和 trace_flags 附加到请求作用域的 logger 上。这让关联字段在 JSON 输出里看得见,但并没有把日志发送到 OpenTelemetry 的 Logs 管道。这个区别很关键。一位回应 HaloLog 第一篇文章的用户把话说得很清楚:他们想要的是把日志发送到 OpenTelemetry,而不只是在消息旁边打印出 trace 信息。 不久之后,Daniel Loader 提交了 HaloLog 的第一个外部 pull request。这个 PR 增加了一个输出适配器,把 HaloLog 的 LogEntry 映射成 OpenTelemetry 的 LogRecord,映射内容包括时间戳、严重级别、正文、结构化字段和上下文、组件/错误/调用方元数据,以及可选的重建 trace context。Daniel Loader 署名的五个提交保留在合并历史里。维护者随后对架构和测试做了加固。 最终形成的路径是这样的:一个 HaloLog LogEntry 扇出到控制台或文件输出,同时进入独立的 otelbridge 模块;只测适配器的基准测试在 SDK 处理和导出之前就结束了。 otelbridge 有自己的 go.mod,不导入它的程序不会把 OpenTelemetry 拉进 HaloLog 的核心依赖图;导入它的程序可以把桥接器注册在已有的控制台或文件适配器旁边,让正常的扇出把同一条逻辑条目送到两个目的地。扇出是顺序的、尽力而为的,它不会让跨 sink 的投递变成原子操作,不会把每个适配器的写入错误传播给日志调用方,也不会让各个 sink 的线上表示完全一致。 映射字段是必要的,但并不充分。真正的工作量,都藏在那些契约里。
热门跟贴