Skip to main content
This guide adds a second span exporter to the OpenTelemetry Node SDK. Your current exporter keeps sending full traces to your APM. The new one sends the same spans to Lumenstat, which keeps only counts, status codes and latency per route. The setup is the one in examples/test-api, which the repository uses to test the API path end to end.
1

Issue an ingest key

Issue a key on your project’s Setup page, as described in Issue an ingest key. Make it available to your service as an environment variable:
The variable name is yours to choose. Lumenstat only sees the header value.
2

Install the packages

The example pins @opentelemetry/sdk-node 0.300.0, @opentelemetry/sdk-trace 3.0.0, @opentelemetry/exporter-trace-otlp-http 0.300.0, @opentelemetry/instrumentation-http 0.300.0 and @opentelemetry/instrumentation-express 0.72.0.
On an older SDK that uses @opentelemetry/sdk-trace-base 1.x or 2.x, the processor takes the exporter as its first argument: new BatchSpanProcessor(exporter). Everything else on this page is the same.
3

Add the second exporter

Create instrument.cjs. Replace the first processor’s exporter with the one you use today.
instrument.cjs
spanProcessors replaces traceExporter and the OTEL_TRACES_EXPORTER variable. If your setup used traceExporter: yourExporter, move that exporter into the array as shown, or your APM stops receiving traces.
4

Load it before your app

The file must run before Express or Nest is required, so the instrumentations can patch them.
--require hooks CommonJS require calls. If your app loads Express as an ES module, the Express instrumentation does not patch it unless you also register OpenTelemetry’s ESM loader hook. Without it, server spans carry no http.route and Lumenstat falls back to the raw path.
5

Confirm data is arriving

Send a few requests to your service, wait about a minute, then reload Setup. See Confirm data is arriving below.

Why metrics and logs are set to none

When NodeSDK gets no metric readers or log processors in code, it reads OTEL_METRICS_EXPORTER and OTEL_LOGS_EXPORTER, and both default to otlp. The SDK would then export its own metrics every 60 seconds, and any OpenTelemetry logs, to OTEL_EXPORTER_OTLP_ENDPOINT, or to http://localhost:4318 when that is unset. Those exports carry no ingest key. Lumenstat does not need them: the collector builds request metrics from your spans. The ??= operator keeps any value you already set. If your APM receives metrics or logs through these variables, keep your settings. They go to your APM’s endpoint, not to Lumenstat. Set both lines before new NodeSDK(). The SDK reads the metrics variable in its constructor.

Keep the two exporters apart

The Node OTLP exporters merge OTEL_EXPORTER_OTLP_HEADERS and OTEL_EXPORTER_OTLP_TRACES_HEADERS into every exporter they build, including ones you configure in code. In a dual-export setup:
  • Do not set OTEL_EXPORTER_OTLP_TRACES_HEADERS to the ingest key, as the Copy settings lines on Setup do. Your APM would receive your Lumenstat key.
  • Do not keep APM credentials in OTEL_EXPORTER_OTLP_HEADERS. Lumenstat would receive them. Pass them in code on the APM exporter, as in the example.
The Setup lines are meant for a service whose only exporter is Lumenstat. See Use SDK environment variables.

Get the best routes with http.route

Lumenstat names routes from the http.route attribute on the server span when it is present, and normalizes the raw path only when it is not. A route template is the best name a request can have.
  • Express. The Express instrumentation records the route that matched, and the HTTP instrumentation writes it to http.route on the server span. With app.get('/accounts/:account', ...), a request to /accounts/GA5Y… is counted as GET /accounts/:account.
  • NestJS. On Nest’s default Express platform, Nest registers each controller route on Express, so the Express instrumentation names it the same way. A handler with @Get(':account') in @Controller('accounts') is counted as /accounts/:account. The Nest instrumentation adds controller spans for your APM. Lumenstat ignores them because they are not server spans.
  • Nest on Fastify, or a plain node:http server. The Express instrumentation does not apply, and the server span carries only the raw path. Lumenstat then applies its normalization rules, which handle Stellar addresses, ids, hashes and tokens.
To name routes yourself on a server without a router, set the attribute on the active span. The HTTP instrumentation runs your handler inside the server span, and it leaves an http.route you set in place. This needs @opentelemetry/api.

Confirm data is arriving

Open Setup in the dashboard. The Is data arriving? card has an API line: The check runs when the page loads, so reload it. The SDK’s batch processor exports every 5 seconds by default and the collector flushes aggregates every 15 seconds, so allow up to a minute after the first request. Then open API to see each route with its requests, status codes and latency.
  • Check that LUMENSTAT_INGEST_KEY reaches the process. With an empty or wrong key the collector still answers with success and the writer drops the data, so nothing fails visibly.
  • Check that the service can reach https://otlp.lumenstat.com on port 443.
  • Start the service with OTEL_LOG_LEVEL=debug to print exporter errors.
  • Make sure the requests reach an instrumented HTTP server. Only server spans are counted.
spanProcessors replaces traceExporter and OTEL_TRACES_EXPORTER. Put your APM exporter in the spanProcessors array next to the Lumenstat one.
The SDK is exporting metrics or logs with its defaults. Set OTEL_METRICS_EXPORTER=none and OTEL_LOGS_EXPORTER=none before new NodeSDK(), or point those exporters at your APM.
The server spans have no http.route. Check that the Express instrumentation is registered and that the app loads Express through require. {unknown} means the span had no route or path attribute at all.
A sampler such as OTEL_TRACES_SAMPLER=traceidratio applies to both exporters, so Lumenstat sees only the sampled requests. Keep the SDK unsampled and sample in a Collector pipeline that feeds only your APM. See Send only what Lumenstat reads.
Last modified on November 14, 2026