In the demo trace, the slowest span is the reverted confirm: 264.9 ms, against 11.8 ms for the successful one. It also fetches the revert reason. In a short script, it’s the span most likely to miss the backend.
What does an exporter do?
It takes finished spans from a span processor and sends them somewhere. The usual pair in Node.js:
| Piece | Job |
|---|---|
BatchSpanProcessor |
Collects finished spans, exports them in batches on a timer |
OTLPTraceExporter |
Encodes them as OTLP and sends them over HTTP or gRPC |
OTEL_EXPORTER_OTLP_ENDPOINT |
Where they go |
OTEL_EXPORTER_OTLP_HEADERS |
Auth, if the backend needs it |
Node.js has three OTLP trace exporter packages: exporter-trace-otlp-proto (HTTP with protobuf), -http (HTTP with JSON) and -grpc. The hashspan demo uses -proto with a batch processor, and switches to the console exporter when OTEL_TRACES_EXPORTER=console is set.
Jaeger or Langfuse?
Both speak OTLP over HTTP, so only the variables change:
# Jaeger, localOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Langfuse Cloud (EU)OTEL_EXPORTER_OTLP_ENDPOINT=https://cloud.langfuse.com/api/public/otelOTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64 of public-key:secret-key>"The Langfuse values are from its OpenTelemetry docs. Langfuse doesn’t take gRPC yet, so use the -proto or -http exporter. In both cases the exporter adds /v1/traces to the base URL.
Why do spans vanish at exit?
The batch processor waits before it exports, and a script can end before it does. Here it is, run twice against a local Jaeger (@opentelemetry/sdk-trace-node@2.11.0, exporter-trace-otlp-proto@0.222.0), once without SHUTDOWN and once with it:
import { trace } from '@opentelemetry/api';import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';import { resourceFromAttributes } from '@opentelemetry/resources';import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
const provider = new NodeTracerProvider({ resource: resourceFromAttributes({ 'service.name': process.env.SVC ?? 'learn-otlp' }), spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())], // reads OTEL_EXPORTER_OTLP_ENDPOINT});provider.register();
trace.getTracer('learn-demo').startSpan('send 8453').end();
if (process.env.SHUTDOWN) await provider.shutdown(); // exports what the batch still holds| Run | Jaeger |
|---|---|
| No shutdown | “No traces found” |
await provider.shutdown() |
The span is there |
No error either way. The span just isn’t there.