Skip to content
telemetry.dev
Esc
↑↓navigate↵open⌘Jpreview
On this page

OpenTelemetry

Connect an existing OpenTelemetry setup to telemetry.dev with a span processor or a standard OTLP/HTTP exporter.

Use this page when your application already sets up OpenTelemetry and you want to keep that setup. There are two ways to connect it to telemetry.dev:

  • Add the telemetry.dev span processor to your tracer provider, in TypeScript or Python. Besides exporting spans, the processor adds the user, session, and metadata attributes from propagateAttributes() and records GenAI metrics.
  • Point a standard OTLP/HTTP exporter at the telemetry.dev ingest API. This works from any language and needs no telemetry.dev package.

If your application does not use OpenTelemetry yet, the TypeScript SDK or Python SDK is simpler: it sets up its own provider.

Add the span processor in TypeScript

@telemetry-dev/otel provides TelemetrySpanProcessor. It needs Node.js 20.19.0 or later and is ESM-only.

npm install @telemetry-dev/otel @opentelemetry/api
pnpm add @telemetry-dev/otel @opentelemetry/api
yarn add @telemetry-dev/otel @opentelemetry/api
bun add @telemetry-dev/otel @opentelemetry/api

Create a project at telemetry.dev and copy an API key from the project setup page. Keys look like td_live_....

TELEMETRY_DEV_API_KEY=td_live_...
OTEL_SERVICE_NAME=my-service
TELEMETRY_DEV_ENVIRONMENT=production

Add the processor wherever your setup lists span processors. The examples below assume you already have the setup’s own package installed.

NodeSDK

import { NodeSDK } from "@opentelemetry/sdk-node";
import { TelemetrySpanProcessor } from "@telemetry-dev/otel";

const sdk = new NodeSDK({
  spanProcessors: [new TelemetrySpanProcessor()],
});

sdk.start();

Call sdk.shutdown() when the process stops cleanly, so the last batch is sent.

@vercel/otel

import { registerOTel } from "@vercel/otel";
import { TelemetrySpanProcessor } from "@telemetry-dev/otel";

export function register() {
  registerOTel({
    serviceName: "my-service",
    spanProcessors: [new TelemetrySpanProcessor()],
  });
}

BasicTracerProvider

import { trace } from "@opentelemetry/api";
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
import { TelemetrySpanProcessor } from "@telemetry-dev/otel";

const provider = new BasicTracerProvider({
  spanProcessors: [new TelemetrySpanProcessor()],
});

trace.setGlobalTracerProvider(provider);

Your provider’s resource describes your spans, not the processor. Set service.name and deployment.environment.name on that resource. If your API key is limited to one environment, deployment.environment.name must match it, or the ingest API rejects the request. See Authentication.

Processor options

new TelemetrySpanProcessor(options) accepts these options:

Option Type Environment fallback Default Purpose
apiKey string TELEMETRY_DEV_API_KEY None Authenticates exports.
baseUrl string TELEMETRY_DEV_BASE_URL https://ingest.telemetry.dev Sets the ingest URL. Trailing slashes are removed.
exportMode "batched" | "immediate" None "batched" Uses a BatchSpanProcessor or a SimpleSpanProcessor inside.
batch BatchOptions None See the next table Changes the batch processor limits.
spanFilter (span: ReadableSpan) => boolean None None Chooses which ended spans to export.
metrics boolean None true Records automatic metrics. Needs an API key.
serviceName string OTEL_SERVICE_NAME unknown_service Sets service.name on the metrics resource.
environment string TELEMETRY_DEV_ENVIRONMENT production Sets deployment.environment.name on the metrics resource.
fetch typeof fetch None globalThis.fetch Supplies the HTTP function.
onError (error: Error) => void None None Receives the processor’s internal errors.
spanExporter SpanExporter None None Replaces the telemetry.dev span exporter, for tests.

batch merges the values that you supply over these defaults:

Field Type Default
maxExportBatchSize number 64
scheduledDelayMillis number 1000
maxQueueSize number 2048
exportTimeoutMillis number 30000

Without an API key or a spanExporter, the processor does nothing and never throws, so you can attach it unconditionally, for example in local development. A spanExporter turns on span export without an API key, but metrics still need a key.

Choose which spans to export

The processor exports every span that your provider ends. This differs from the TypeScript SDK with registerGlobal: true, which exports only its own spans by default.

To export a subset, pass spanFilter:

new TelemetrySpanProcessor({
  spanFilter: (span) => span.instrumentationScope.name.startsWith("my-app"),
});

If the filter throws, the processor exports the span anyway and reports the error to onError.

Add user, session, and metadata

@telemetry-dev/otel also exports propagateAttributes(). The processor adds its attributes to every span started inside the callback:

import { propagateAttributes } from "@telemetry-dev/otel";

await propagateAttributes(
  {
    userId: "user_123",
    sessionId: "session_456",
    metadata: { tenant: "acme" },
  },
  () => handleRequest(),
);
Field Attribute
userId user.id
sessionId gen_ai.conversation.id
metadata td.metadata.<key>

Calls can nest. The inner call’s values replace outer values that have the same key.

Put each session in one trace

The SDKs put every root span of a session into one trace. The processor cannot do that on its own: it sees spans only after your provider has given them trace IDs. To get the same grouping, start each root span of a session with withSessionParent(), and wrap your provider’s sampler in sessionSampler():

import { context } from "@opentelemetry/api";
import {
  BasicTracerProvider,
  ParentBasedSampler,
  TraceIdRatioBasedSampler,
} from "@opentelemetry/sdk-trace-base";
import { sessionSampler, TelemetrySpanProcessor, withSessionParent } from "@telemetry-dev/otel";

const sampler = new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1) });

const provider = new BasicTracerProvider({
  sampler: sessionSampler(sampler),
  spanProcessors: [new TelemetrySpanProcessor()],
});

const sessionId = "session_456";
const span = provider
  .getTracer("my-app")
  .startSpan(
    "turn",
    {},
    withSessionParent(context.active(), sessionId, process.env.TELEMETRY_DEV_API_KEY),
  );
span.end();

withSessionParent() returns a context whose parent is a session parent derived from the API key and the session ID. telemetry.dev never receives a span for that parent; it only fixes the trace ID. If the API key or session ID is empty, or the context already has a valid span, the function returns the context unchanged.

sessionSampler() keeps your sampling policy for session root spans. Pass it the sampler that your provider would otherwise use. Without an argument, it reads OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG.

Use only the exporter

To keep your own BatchSpanProcessor or SimpleSpanProcessor, use createTelemetrySpanExporter(). It sends OTLP protobuf to telemetry.dev but does not add correlation attributes, filter spans, or record metrics:

import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { createTelemetrySpanExporter } from "@telemetry-dev/otel";

const processor = new BatchSpanProcessor(createTelemetrySpanExporter());

The exporter accepts apiKey, baseUrl, fetch, onError, and exportTimeoutMillis, which defaults to 30,000 ms. apiKey and baseUrl fall back to the same environment variables as the processor. Without an API key, the exporter drops spans.

Export and metrics behavior

The processor records these histograms for the spans it exports:

Metric Recorded for
gen_ai.client.operation.duration Spans with gen_ai.operation.name set to chat, invoke_agent, embeddings, or execute_tool
gen_ai.client.token.usage chat, invoke_agent, and embeddings spans with input or output token counts
gen_ai.client.operation.time_to_first_chunk chat spans with gen_ai.response.time_to_first_chunk

Spans that spanFilter rejects record no metrics. In batched mode, the processor sends metrics every 60 seconds. In immediate mode, it sends them only when your provider flushes or shuts down.

The processor handles spans only. Send logs through your own OpenTelemetry log pipeline.

Add the span processor in Python

The telemetry-dev package includes TelemetrySpanProcessor for a TracerProvider that you own. It needs Python 3.10 or later.

pip install telemetry-dev
uv add telemetry-dev

Set the same environment variables as for TypeScript, then add the processor to your provider:

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from telemetry_dev.otel import TelemetrySpanProcessor

provider = TracerProvider(
    resource=Resource.create(
        {"service.name": "my-service", "deployment.environment.name": "production"}
    )
)
provider.add_span_processor(TelemetrySpanProcessor())
trace.set_tracer_provider(provider)

Call provider.shutdown() when the process stops, so the last batch is sent.

TelemetrySpanProcessor accepts an optional span exporter as its first argument, for tests, and these keyword arguments:

Argument Environment fallback Default Purpose
api_key TELEMETRY_DEV_API_KEY None Authenticates exports.
base_url TELEMETRY_DEV_BASE_URL https://ingest.telemetry.dev Sets the ingest URL. Trailing slashes are removed.
export_mode None "batched" Uses a BatchSpanProcessor or a SimpleSpanProcessor inside.
max_export_batch_size None 64 Spans per export batch.
schedule_delay_millis None 1000 Delay between batch exports.
max_queue_size None 2048 Spans held before new ones are dropped.
export_timeout_millis None 30000 Timeout of each batch export.
span_filter None None Chooses which ended spans to export.
metrics None True Records automatic metrics. Needs an API key.
service_name OTEL_SERVICE_NAME unknown_service Sets service.name on the metrics resource.
environment TELEMETRY_DEV_ENVIRONMENT production Sets deployment.environment.name on the metrics resource.
on_error None None Receives the processor’s internal errors.

The Python processor behaves like the TypeScript one:

  • Without an API key or an exporter, it does nothing.
  • It exports every span unless span_filter rejects it, and a span_filter that raises does not drop the span.
  • It adds the attributes from telemetry_dev.propagate_attributes() to every span started inside the block.
  • It records the same histograms, and handles spans only.

The Python package has no public equivalent of withSessionParent(). With your own provider, each root span starts its own trace, even when it has a session ID.

To keep your own span processor, use create_telemetry_span_exporter(). It accepts api_key, base_url, and timeout in seconds, which defaults to 10.0:

from opentelemetry.sdk.trace.export import BatchSpanProcessor
from telemetry_dev.otel import create_telemetry_span_exporter

provider.add_span_processor(BatchSpanProcessor(create_telemetry_span_exporter()))

Send OTLP/HTTP from any language

You do not need a telemetry.dev package. Point a standard OTLP/HTTP exporter at the trace endpoint, and send your API key as a bearer token:

https://ingest.telemetry.dev/v1/traces
Authorization: Bearer td_live_...

OpenTelemetry SDKs that follow the exporter specification read the standard exporter environment variables. These settings send traces, logs, and metrics to telemetry.dev:

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.telemetry.dev
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20td_live_...

The exporter appends /v1/traces, /v1/logs, or /v1/metrics to the base endpoint. The header value is URL-encoded, so write the space after Bearer as %20.

The ingest API accepts these payload encodings:

Request Content-Type Payload
application/x-protobuf OTLP protobuf
application/json OTLP JSON with lowerCamelCase field names

It accepts gzip and identity content encoding, and it answers in the same encoding as the request.

Example: Node.js with standard OpenTelemetry packages

This example uses only OpenTelemetry packages. It sends spans from a short-lived script:

npm install @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
pnpm add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
yarn add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
bun add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-node
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 apiKey = process.env.TELEMETRY_DEV_API_KEY!;
const baseUrl = (process.env.TELEMETRY_DEV_BASE_URL ?? "https://ingest.telemetry.dev")
  .replace(/\/+$/, "");

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    "service.name": process.env.OTEL_SERVICE_NAME ?? "otel-chat",
    "deployment.environment.name": process.env.TELEMETRY_DEV_ENVIRONMENT ?? "development",
  }),
  spanProcessors: [
    new BatchSpanProcessor(
      new OTLPTraceExporter({
        url: `${baseUrl}/v1/traces`,
        headers: { authorization: `Bearer ${apiKey}` },
      }),
    ),
  ],
});

provider.register();

// Start application work and spans here.

await provider.shutdown(); // Sends the last batch before the script exits.

A short-lived process must flush or shut down its provider before it exits. A long-running service can keep its normal OpenTelemetry lifecycle.

Make GenAI spans recognizable

Set gen_ai.operation.name on each generative AI span, for example chat, embeddings, execute_tool, or invoke_agent. The ingest API reads it to decide what kind of operation a span is. Without it, the ingest API guesses from the other attributes on the span.

Use the OpenTelemetry GenAI semantic conventions where you can. telemetry.dev also recognizes several provider and framework attribute sets. See Span attributes for the fields it reads, and OTLP ingest API for the full wire contract, including body limits and error responses.

Endpoint summary

Signal Endpoint Sent by telemetry.dev packages
Traces POST /v1/traces The TypeScript and Python SDKs, the span processors, and the exporters
Logs POST /v1/logs The TypeScript and Python SDKs, from log()
Metrics POST /v1/metrics The TypeScript and Python SDKs and the span processors

All three endpoints take Authorization: Bearer td_live_... and accept OTLP protobuf or OTLP JSON.

Last updated on October 18, 2026

Was this page helpful?