Skip to content

Architecture

setupTracing assembles a tracer provider, registers it as the global provider, and registers the instrumentations against it. Everything the OpenTelemetry Node SDK needs is configured in one place, so a caller passes a service name and an endpoint rather than a pipeline.

The pipeline

flowchart LR
  A[Application] --> B[Instrumentations]
  B --> C[NodeTracerProvider]
  C --> D[BatchSpanProcessor]
  D --> E[OTLPTraceExporter<br/>gRPC]
  E --> F[Collector]
  B --> G[MeterProvider]
  G --> H[PeriodicExportingMetricReader]
  H --> I[OTLPMetricExporter<br/>gRPC]
  I --> F
  B --> J[LoggerProvider]
  J --> K[BatchLogRecordProcessor]
  K --> L[OTLPLogExporter<br/>gRPC]
  L --> F
  R[Resource detectors] --> C
  R --> G
  R --> J
Stage Component Configuration
Provider NodeTracerProvider Resource from detectors, merged with the explicit service and container attributes
Processor BatchSpanProcessor maxQueueSize 4096, maxExportBatchSize 1024, scheduledDelayMillis 2000, exportTimeoutMillis 10000
Exporter OTLPTraceExporter OTLP over gRPC, timeoutMillis 10000, concurrencyLimit from the options (default 10)
Registration tracerProvider.register() Global tracer provider, context manager and propagators, see Propagation
Meter provider MeterProvider Same resource as the tracer provider, registered as the global meter provider
Metric reader PeriodicExportingMetricReader exportIntervalMillis from the options (default 60000)
Metric exporter OTLPMetricExporter OTLP over gRPC, cumulative temporality, metricsUrl from the options (default the trace endpoint)
Logger provider LoggerProvider Same resource as the tracer provider, registered as the global logger provider
Log processor BatchLogRecordProcessor maxQueueSize 4096, maxExportBatchSize 1024, scheduledDelayMillis 2000, exportTimeoutMillis 10000
Log exporter OTLPLogExporter OTLP over gRPC, logsUrl from the options (default the trace endpoint)

Spans are batched rather than exported one at a time, so a span is visible in the backend up to scheduledDelayMillis after it ends. Shutdown covers the flush a process needs before it exits.

Metrics

The instrumentations record their measurements against whichever meter provider is registered when registerInstrumentations runs. The HTTP and undici instrumentations record request duration histograms, and the AWS SDK instrumentation records Bedrock token usage and operation duration. Where no meter provider is registered, those instruments come from the no-op meter and every measurement is discarded.

setupTracing registers a MeterProvider and passes it to registerInstrumentations, which is what turns those measurements into exported metrics. RuntimeNodeInstrumentation is registered alongside them for the metrics no span can carry: event loop delay and utilisation, garbage collection duration, and heap occupancy.

Recording happens outside the sampler. The HTTP instrumentation records the duration after the span ends, without consulting the sampling decision, so metrics describe every request while traces describe a sampled subset.

Logs

The Pino instrumentation does two separate things. Log correlation adds trace_id, span_id and trace_flags to each record written to the application's own stream. Log sending routes a copy of each record to the OpenTelemetry logs API, and it is on by default.

Log sending reaches a backend only when a logger provider is registered. Where none is, the record is parsed and rebuilt as a LogRecord and then handed to a no-op logger, so the cost is paid on every log line and nothing arrives. setupTracing registers a LoggerProvider, which turns that work into records delivered over OTLP, and sets disableLogSending when enableLogs is false, so the work stops rather than continuing for nothing.

Shared resource

The resource is built once and passed to all three providers. Grafana pairs a metric and a log line with a trace on service.name, which requires them to be identical.

Resource attributes

The resource is built in two steps. Detection runs first, then the explicit attributes are merged on top, so an explicitly passed service name wins over one found by the environment detector.

Source Attributes
envDetector Anything set in OTEL_RESOURCE_ATTRIBUTES
hostDetector host.name, host.arch
osDetector os.type, os.version
processDetector process.pid, process.command, process.runtime.*
serviceInstanceIdDetector service.instance.id
Explicit service.name, and container.name when a hostname is resolved

container.name is omitted entirely when no hostname is passed and neither CONTAINER_NAME nor HOSTNAME is set, rather than written as an undefined value.

Propagation

register() is called without overrides, which installs the AsyncLocalStorageContextManager and a composite propagator of W3C Trace Context and W3C Baggage. An incoming request carrying traceparent continues the caller's trace, and every outgoing HTTP or fetch call injects one.

This is what pairs a caller's client span with the callee's server span. A service graph is built from those pairs, so propagation is the prerequisite for one.

Service graph attributes

Tempo names a service graph node from peer.service, and no instrumentation emits that attribute. The library sets it in the request hooks instead.

Call Where peer.service comes from
Outgoing HTTP The host of the outgoing ClientRequest, matched against the known peer list
Outgoing fetch The origin of the undici request, matched against the same list
IORedis Set to redis unconditionally
AWS SDK The AWS service name from the request, lower cased

The known peer list holds elasticsearch and redis. A host containing either substring sets both peer.service and db.system.name to that value.

Only outgoing requests carry host, so the HTTP hook returns without setting anything when there is none. That keeps server spans out of the graph as peers, where peer.service has to name the remote service being called rather than the local one.

Idempotency

The provider is held in module scope. setupTracing returns early when it is already set, logging a warning and returning a tracer from the existing provider, so repeated initialisation cannot register a second set of instrumentations or a second exporter against the same process.

stopTracing shuts each provider down in turn, then clears every module scope reference and unregisters the global meter and logger providers, since the API will not replace either while one is in place. A later setupTracing call therefore builds a fresh pipeline. Shutdown covers what each step flushes.

Diagnostics

The OpenTelemetry diagnostic logger is set to a console logger at INFO level when the module is imported, before setupTracing runs. Exporter failures, instrumentation warnings and the messages this library emits are all written to the console through it.