Configuration¶
Installation¶
npm install @saidsef/tracing-node --save
The package is ESM only and declares "type": "module". Node 24.0.0 or later is required.
Initialisation order¶
Instrumentation works by patching modules as they are loaded, so setupTracing has to run before the application requires or imports the libraries being traced. Calling it after Express or IORedis has been loaded leaves those modules unpatched and produces no spans for them.
ESM application¶
import {setupTracing} from '@saidsef/tracing-node';
setupTracing({serviceName: 'my-service', url: 'http://alloy:4317'});
const {default: app} = await import('./app.mjs');
Preload¶
// instrument.mjs
import {setupTracing} from '@saidsef/tracing-node';
setupTracing();
node --import ./instrument.mjs ./app.cjs
The preload form is the reliable one. --import runs the module to completion before the application entry point loads, and it works for a CommonJS application, where require-in-the-middle patches each module on require. The end to end harness uses this form.
Options¶
setupTracing({
hostname: 'pod-abc123',
serviceName: 'my-service',
url: 'http://alloy:4317',
concurrencyLimit: 10,
enableFsInstrumentation: false,
enableDnsInstrumentation: false,
enableMetrics: true,
metricsUrl: 'http://alloy:4317',
metricExportIntervalMillis: 60000,
enableLogs: true,
logsUrl: 'http://alloy:4317',
});
| Option | Type | Description | Required | Default |
|---|---|---|---|---|
hostname |
string | Container or pod hostname, recorded as container.name |
No | CONTAINER_NAME, then HOSTNAME |
serviceName |
string | Service name, recorded as service.name |
Yes | SERVICE_NAME |
url |
string | Collector endpoint, <scheme>://<host>:<port> |
Yes | ENDPOINT |
concurrencyLimit |
number | Concurrent exports the exporter allows | No | 10 |
enableFsInstrumentation |
boolean | Enable file system instrumentation | No | false |
enableDnsInstrumentation |
boolean | Enable DNS instrumentation | No | false |
enableMetrics |
boolean | Register a meter provider and export metrics | No | true |
metricsUrl |
string | Metrics endpoint, when it differs from the trace endpoint | No | url |
metricExportIntervalMillis |
number | Interval between metric exports | No | 60000 |
enableLogs |
boolean | Register a logger provider and export Pino log records | No | true |
logsUrl |
string | Logs endpoint, when it differs from the trace endpoint | No | url |
setupTracing throws Error: serviceName is required or Error: url is required when neither the option nor its environment variable supplies a value.
Environment variables¶
| Variable | Maps to | Required |
|---|---|---|
SERVICE_NAME |
serviceName |
Yes, unless the option is passed |
ENDPOINT |
url |
Yes, unless the option is passed |
CONTAINER_NAME |
hostname |
No |
HOSTNAME |
hostname, when CONTAINER_NAME is unset |
No |
An option passed to setupTracing takes precedence over the matching environment variable. OTEL_RESOURCE_ATTRIBUTES is read by the environment resource detector, and any service.name it carries is overridden by the explicit one.
Metrics¶
Metrics are exported by default, over OTLP gRPC, to the same endpoint as traces. An OpenTelemetry Collector or Grafana Alloy accepts all signals on port 4317, so a single endpoint covers both. Point metricsUrl elsewhere where the trace endpoint takes traces alone, for example a Tempo OTLP receiver, and set enableMetrics to false where metrics are not wanted at all.
Aggregation temporality is cumulative, which is what Prometheus and Mimir expect. OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE overrides it.
The export interval is 60 seconds. A shorter interval raises resolution and the volume written to the backend in equal measure.
Sampling does not reach the metrics, so OTEL_TRACES_SAMPLER can be turned down without losing accuracy. Metrics covers why.
Logs¶
Pino log records are exported by default, over OTLP gRPC, to the same endpoint as traces. Each record carries the trace id and span id of the request that wrote it, which is what links a log line to its trace in Grafana. The application keeps writing to its own stream, so container logs are unchanged.
Setting enableLogs to false disables log sending at the instrumentation, rather than leaving each record to be built and then discarded. Logs covers that path.
Point logsUrl elsewhere where the trace endpoint does not accept logs.
Log export covers Pino alone. An application logging through anything else is unaffected by these options.
Optional instrumentations¶
enableFsInstrumentation and enableDnsInstrumentation are off by default. Both instrumentations patch on construction, so each is constructed only when its option is set. File system tracing in particular produces a large number of spans and is worth enabling only while investigating file access. Instrumentation covers what each emits.
Using the returned tracer¶
setupTracing returns a tracer for the service, which creates manual spans for work no instrumentation covers.
const tracer = setupTracing({serviceName: 'my-service', url: 'http://alloy:4317'});
await tracer.startActiveSpan('reconcile', async (span) => {
try {
await reconcile();
} finally {
span.end();
}
});
A span created this way is a child of whatever span is active in the current context, so a manual span inside a request handler joins that request's trace.
Shutdown¶
import {setupTracing, stopTracing} from '@saidsef/tracing-node';
process.on('SIGTERM', async () => {
await stopTracing();
process.exit(0);
});
stopTracing awaits the tracer provider shutdown, which flushes the batch span processor, then the meter provider shutdown, which flushes a final metric export, then the logger provider shutdown, which flushes queued log records. Each is awaited separately, so a failing exporter on one signal still lets the others flush. The providers are then cleared, so a later setupTracing call builds a fresh pipeline. stopTracing logs a warning and returns when tracing was never initialised, and logs an error rather than throwing when shutdown fails.
Spans and log records are batched, and metrics are exported on an interval, so a process that exits without this loses whatever is still queued.
Repeated initialisation¶
A second setupTracing call logs Tracing is already initialized. Returning existing tracer. and returns a tracer from the existing provider. The options passed to the second call are ignored, apart from serviceName, which names the returned tracer.