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/apipnpm add @telemetry-dev/otel @opentelemetry/apiyarn add @telemetry-dev/otel @opentelemetry/apibun add @telemetry-dev/otel @opentelemetry/apiCreate 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-devuv add telemetry-devSet 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_filterrejects it, and aspan_filterthat 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-nodepnpm add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-nodeyarn add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-nodebun add @opentelemetry/api @opentelemetry/exporter-trace-otlp-proto @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-nodeimport { 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.