Build1 publisher2 min readPublished
HaloLog reconstructs span context before probing whether OpenTelemetry wants the record
The otelbridge adapter builds trace context first so the enablement check and the Emit call see the same values, because a processor downstream may filter on the sampled flag that a cheap probe would drop.
The Engineer · Build desk

What happened
- Daniel Loader opened HaloLog's first external pull request, adding an output adapter that maps a HaloLog LogEntry into an OpenTelemetry LogRecord, with five authored commits still in the merge history.
- HaloLog already had an optional Bind helper that attached trace_id, span_id and trace_flags to request-scoped loggers, which showed correlation fields in JSON output without sending logs to OpenTelemetry.
- otelbridge ships as its own Go module, pinned at v1.0.3 for these measurements, so a program that never imports it keeps OpenTelemetry out of HaloLog's core dependency graph.
- The adapter derives the record's context from the entry first, then calls logger.Enabled with that context and the severity, and returns early when the logger says no.
- Fan-out across registered adapters is sequential and best-effort: delivery across sinks is not atomic, the logging caller does not get back a per-adapter write error, and the wire output is not identical.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint An admission check that must see the record's real context means the correlation work cannot be deferred, so a bridge pays field decoding even on records the pipeline then throws away.
- exposure A write that fails inside the OpenTelemetry adapter still looks like a successful log call to the application, so operators reconciling the console copy against the backend are comparing two different byte streams with no error to guide them.
- decision Because the bridge is a separate module, adoption is a per-binary choice made by whoever imports it, and teams that decline keep their dependency audit unchanged.
That order is required by a contract. OpenTelemetry exposes Logger.Enabled so a bridge can skip expensive record construction when the pipeline does not want a record, and the method accepts the context that will be associated with the record [18]. The cheap implementation probes with context.Background(), builds the record, reconstructs the span context, then calls Emit with the richer context [19]. A processor may filter using the sampled flag or another value carried by that context, so that bridge tests admission under one context and emits under another [20].
The encoding work around it is the easy half. Message becomes Body, level maps to Severity, ordinary fields become Attributes, valid correlation fields can become trace context, and then the bridge calls Emit [3]. The OpenTelemetry data model also distinguishes ObservedTimestamp, Resource, InstrumentationScope and EventName, and the selected provider and SDK supply observed time, resource and scope handling [17].
The post lists four contracts it had to preserve across the API boundary: the context used to ask whether a record is enabled, ownership of a pooled entry after it is returned, masking precedence when a field has more than one storage form, and the lifecycle of a provider the bridge does not own [4]. Each has a matching production failure: the wrong record emitted, a value leaked that was supposed to be masked, memory retained that the bridge does not own, and the final log lost before the process exits [5]. The provider itself arrives at registration, passed in through otelbridge.WithLoggerProvider [22].
Part 1 of the series examined what a 23.9 ns logging benchmark does and does not prove [1]. Part 2 draws its own measurement boundary: each timed operation calls Adapter.Write on a LogEntry built before the timer and ends at an OpenTelemetry API Logger whose Emit is a no-op [6]. Nine layers are named as excluded, among them masking, console and file fan-out, the SDK, processors, exporters, the collector, the network and the backend [7][8]. For the adapter figure to transfer to your service, your Emit would have to do nothing as well. That rules out exporting the logs.
The bridge exists because the earlier helper did not satisfy the people who asked for it. According to the write-up, a user responding to the first HaloLog article wanted logs sent to OpenTelemetry; trace information printed beside a message did not do it [10]. The adapter that followed maps the timestamp, severity, body, structured fields and context, component, error and caller metadata, and optional reconstructed trace context [12].
What to watch
- Whether a later release publishes end-to-end numbers with a real exporter attached. Those numbers would make the adapter figures checkable against a working pipeline.
- Whether the project documents how masking precedence resolves when a field has both a raw and a masked storage form.
- Whether the fan-out ever gains a way for callers to learn that one adapter's write failed.