evlog and OpenTelemetry: connect logs to traces
You already export traces to an OpenTelemetry Collector and want application logs beside them. evlog can build those log events and send them through OTLP. Keep the tracing instrumentation: evlog does not create spans or metrics.
Assign each layer a job
Application fields -> evlog event -> OTLP HTTP /v1/logs -> Collector -> backend
Active OTel span -> traceId + spanId on the event
OTel SDK -> spans and metrics -> Collector -> backend
The SDK instruments operations, the Collector receives and processes telemetry, and the backend provides storage, queries, and alerts. OpenTelemetry's documentation describes those responsibilities. Semantic conventions standardize shared attributes, while custom application attributes remain valid.
Export an event with the active span context
This example assumes a Node.js application with its OpenTelemetry SDK and context manager already initialized. The function must run inside an active span to attach trace context. Follow the JavaScript instrumentation guide if tracing is not configured yet.
Install the logging package and OTel API in that application:
npm install evlog @opentelemetry/api
The function writes a completed checkout event as local JSON and exports it through OTLP. It awaits delivery explicitly, so the caller can observe an export failure. Do not also register this drain globally, which would export the event twice.
import { isSpanContextValid, trace } from '@opentelemetry/api'
import { createLogger, initLogger } from 'evlog'
import { createOTLPDrain } from 'evlog/otlp'
initLogger({ env: { service: 'checkout' }, pretty: false })
const drain = createOTLPDrain({ recordShape: 'compact' })
export async function recordCheckout(orderId: string, outcome: 'paid' | 'declined') {
const log = createLogger({ orderId, outcome })
const spanContext = trace.getActiveSpan()?.spanContext()
if (spanContext && isSpanContextValid(spanContext)) {
log.set({ traceId: spanContext.traceId, spanId: spanContext.spanId })
}
if (outcome === 'declined') log.error(new Error('Payment declined'))
const event = log.emit()
if (event) await drain({ event })
}
Configure the OTLP HTTP base endpoint in the application's environment:
OTLP_ENDPOINT=http://localhost:4318
Load that file through your application or Node's --env-file option. A file named .env alone does not configure an arbitrary Node.js process.
For a local Collector, enable a logs pipeline. This minimal configuration prints received records through the debug exporter:
receivers:
otlp:
protocols:
http:
endpoint: 127.0.0.1:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [otlp]
exporters: [debug]
Run the Collector with this configuration and call recordCheckout('order-123', 'paid') inside an instrumented operation. A Collector in a separate container needs a reachable receiver address and port mapping. Use the loopback configuration above for processes on the same host.
Verify correlation before shipping
Find the record by orderId. Its resource should contain service.name: checkout, and its traceId and spanId should match the active span from the same operation. Without a valid active span the event still exports, but those identifiers are absent.
The drain maps level and timestamp to OTLP severity and time fields. With recordShape: 'compact', nested application fields become dotted attributes. The default JSON shape uses a serialized event body and serializes nested attribute values. The OTLP adapter reference describes the mapping and delivery options.
The TraceContext enricher can read an incoming traceparent, but its parent span identifier is not necessarily the active server span. Use the active SDK context as above when you need that exact association.
Decide what to sample and how to deliver
Event sampling in evlog and trace sampling in OpenTelemetry operate on different signals. A retained log can reference a trace that your tracing policy discarded. Coordinate the policies before relying on navigation from every error log to a stored trace.
This example waits for the network request. For production handler integration, configure the drain pipeline and the framework's background-delivery lifecycle deliberately. The adapter supports OTLP HTTP logs, not a gRPC-only endpoint. Serialization, batching, retries, and export add costs that event-construction microbenchmarks do not measure. See Performance for those boundaries.
If you use Nitro, the integration can invoke createOTLPDrain() through evlog:drain after evlog is enabled. Keep your existing OTel instrumentation in either setup. For a library-to-library decision, continue with evlog vs other loggers.