Logging bridges: send your existing logger's lines as OpenTelemetry logs
Pino, Bunyan, Winston, Python logging, slog, Logback and Deno's console as log records with severity, attributes and the request's trace id.
A log line becomes useful next to a trace when it is an OpenTelemetry log record: a severity, a body, structured attributes, and the trace id and span id of the request it was written in. A bridge turns your logger's calls into those records while your normal output (stdout, files) keeps working. You keep your logger; you do not log through an OpenTelemetry API.
By runtime#
| Runtime | Logger | How it is bridged | Run by Belvador |
|---|---|---|---|
| Node.js | Pino | Automatic with the auto-instrumentations | Yes |
| Node.js | Bunyan | Automatic with the auto-instrumentations | No |
| Node.js | Winston | Install @opentelemetry/winston-transport next to Winston | No |
| Node.js | console.* | Not captured — use a logger | — |
| Python | logging (and libraries on it) | OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true with opentelemetry-instrument | Yes |
| Go | log/slog | otelslog handler; call the …Context functions with the request's context | Yes |
| Java | java.util.logging | Automatic with the Java agent | Yes |
| Java | Logback, Log4j 2 | Automatic with the Java agent | No |
| Deno | console.* | Automatic with OTEL_DENO=true | Yes |
| Any | Log files | Collector filelog receiver | No |
"Run by Belvador" means the certification run used that logger and checked severity and trace linking; the others use the same mechanism of the same packages but were not part of the run. Setup lines for each are on the runtime pages: Node.js, Python, Go, Java, Deno, Collector.
What makes a log linked to its trace#
The bridge copies the active span's ids into the record. A record therefore carries a trace id only when it is written while a span is active in that code path:
Inside a request handler — the incoming request's server span is active, so lines written there are linked.
In background work (timers, queue consumers, startup) there is often no active span, so lines have no trace id. That is normal; they are still stored and searchable.
Go has no implicit context: pass the request's
ctx(logger.InfoContext(ctx, …)).Text that mentions a trace id is not a link. A logger that writes
trace_id=…into the message string leaves the record's own trace fields empty.
In Belvador, a linked record shows its trace on the Logs page and appears on the trace page's Logs tab and on an issue's Logs around this error.
Severity#
Bridges map each logger's levels to OpenTelemetry severities (TRACE … FATAL); Belvador's severity filter uses those. Custom levels map to the nearest standard one, so check the result once if your logger defines its own.
Structured fields#
Fields you pass as an object (logger.info({ orderId }, "paid"), logging.info("paid", extra={"order_id": …}), slog.Int("order_id", …)) become attributes you can filter on, and privacy rules see their names — see Privacy. Values interpolated into the message text are only text.
How much to send#
The bridge sends what your logger's level lets through. Keep production at info or warn; logs are not reduced by trace sampling (Sampling and cost).
Spotted something wrong or missing? Tell us: hello@belvador.dev