docs

Send data from any OpenTelemetry SDK

Point any OpenTelemetry SDK or Collector at Unhandled over OTLP/HTTP, with the endpoint paths, headers, encodings, limits and error codes.

unhandled accepts standard OTLP, so any OpenTelemetry SDK or Collector can send it traces, logs and metrics. This page is the wire contract: the endpoints, the authentication headers, what the server accepts and how it reads what you send. It also has setup code for Python, Go, Ruby, Java and .NET. In Node.js, the Node.js SDK and its framework packages configure all of this for you, and so does the AdonisJS agent.

Endpoints

Send POST requests to the origin of your DSN:

SignalPath
Traces/v1/traces
Logs/v1/logs
Metrics/v1/metrics
Request partAccepted values
content-typeapplication/x-protobuf, application/protobuf or application/json
content-encodinggzip, or none
Body size10 MB as sent, 16 MB after gzip decompression
Items10,000 per table per request, 128 attributes per item. See ingest limits

Use protobuf for server apps. It is smaller, and the official exporters default to it. JSON suits hand-written senders and browsers. Turn gzip on.

The DSN

Each service has its own DSN:

https://[email protected]/0b6f3c1e-8d2a-4f7b-9c5e-2a1d4e6f8b3c
DSN partBecomes
Scheme, host and portThe ingest endpoint. Append the signal path to it: https://ingest.example.com/v1/traces.
UsernameThe API key, uh_live_... or uh_test_..., sent as x-api-key.
PathThe service id, a UUID, sent as x-unhandled-service-id without the leading /.

A DSN with a password, a query string or a fragment is invalid. The DSN carries no environment. You choose it per deployment.

Headers

Every request needs all three:

x-api-keystringrequired

The API key from the DSN username.

x-unhandled-service-iduuidrequired

The service id from the DSN path.

x-unhandled-envproduction | staging | developmentrequired

The environment to file the data under.

The environment comes only from x-unhandled-env. The server overwrites any deployment.environment.name resource attribute with the header value.

Connect your SDK

Pick your stack. Each example splits UNHANDLED_DSN into the endpoint and the headers as described above.

The Python, Go and Ruby tabs show the telemetry file of an example app in the Unhandled repository, under examples/python, examples/go and examples/ruby. Each one reads UNHANDLED_DSN and UNHANDLED_ENV, sets service.name and service.version, and exports over OTLP/HTTP with protobuf and gzip. CI runs each app against a real ingest server and checks that its routes, users, queries, exceptions and release arrive. The Java and .NET tabs use the standard OTEL_* variables and are not tested in CI.

Without code. The Node auto-instrumentation package reads the standard OTEL_* variables. Set the variables from the Any OTel SDK tab, then:

npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
Terminal
node --import @opentelemetry/auto-instrumentations-node/register server.js

In code. @unhandled/node builds this setup for you. If you build the NodeSDK yourself, @unhandled/protocol parses the DSN and builds the headers, so UNHANDLED_DSN is the only secret you keep.

npm install @unhandled/protocol @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-proto @opentelemetry/exporter-logs-otlp-proto @opentelemetry/exporter-metrics-otlp-proto @opentelemetry/sdk-logs @opentelemetry/sdk-metrics @opentelemetry/resources
otel.ts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-proto'
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto'
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
import { resourceFromAttributes } from '@opentelemetry/resources'
import { OTLP_PATHS, exportHeaders, parseDsn } from '@unhandled/protocol'

const parsed = parseDsn(process.env.UNHANDLED_DSN ?? '')
if (!parsed.ok) {
  throw new Error(`Invalid UNHANDLED_DSN: ${parsed.error.code}`)
}
const { dsn } = parsed
const headers = exportHeaders(dsn, 'production')

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    'service.name': 'orders-api',
    'service.version': process.env.RELEASE ?? 'unknown',
  }),
  traceExporter: new OTLPTraceExporter({ url: dsn.endpoint + OTLP_PATHS.traces, headers }),
  logRecordProcessors: [
    new BatchLogRecordProcessor(
      new OTLPLogExporter({ url: dsn.endpoint + OTLP_PATHS.logs, headers })
    ),
  ],
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({ url: dsn.endpoint + OTLP_PATHS.metrics, headers }),
  }),
  instrumentations: [getNodeAutoInstrumentations()],
})

sdk.start()

Load the file before the rest of the app, for example with node --import ./otel.js server.js. The instrumentations have to patch modules before your code imports them.

How Unhandled reads your data

  • Service. Data always lands in the service named by x-unhandled-service-id. The server overwrites the service.name resource attribute with that service's telemetry name.
  • Environment. Read from x-unhandled-env only.
  • Release. service.version becomes the release shown on traces and issues. Unhandled ignores the placeholders 0.0.0, 0.0.1, 1.0.0, unknown, dev and latest. When a new version takes over a service's entry spans, Unhandled records a deployment. Send vcs.ref.head.revision with the full 40-character commit SHA, above all when the version is a semver tag, so the deployment links to its commit.
  • Instance. service.instance.id, or host.name when it is missing, identifies the process for system metrics.
  • Reserved attributes. The server sets unhandled.organization.id, unhandled.project.id, unhandled.environment and unhandled.service.id, and overwrites any values you send.
  • Legacy names. The server renames older attributes such as http.method and db.statement to their current names.

No resource attribute is required, but the views key off attribute names. Semantic conventions lists what each one drives.

What each view reads

The views do not need an Unhandled SDK. Each one reads an OpenTelemetry semantic convention where one exists, and a documented attribute where none does.

ViewRead fromNotes
RequestsSERVER spans with http.request.method, grouped by http.routeWithout http.route, Unhandled uses url.path with identifiers replaced by :id. Old names such as http.method and http.target are renamed at ingest.
JobsSpans with messaging.systemNamed by messaging.destination.name when no Unhandled job name is set. PRODUCER spans, or a messaging.operation.type of send, publish or create, count as dispatches. Old messaging.operation and messaging.destination are renamed.
CommandsA span with unhandled.entry_point=cli, or an INTERNAL span with the span attribute process.executable.nameNamed by cli.command.name, then process.command, then process.executable.name, then the span name. A resource-level process.executable.name does not count.
Usersuser.id, user.email and user.name on SERVER spansenduser.id is renamed to user.id, and enduser.role to user.roles.
ExceptionsSpan events named exception, and log records with exception.type or exception.messageUnhandled parses exception.stacktrace for Python, Go, Ruby, Java and JavaScript. A log record becomes an occurrence with mechanism log_record and handled: true, and its level comes from the record's severity.
DeploymentsThe service.version resource attributevcs.ref.head.revision links the release to its commit. Placeholders such as 0.0.0 are ignored.
EmailsSpans with email.systemNo semantic convention exists. Set email.system, plus email.transport and email.message.subject for the breakdowns.
AnalyticsLog records from the logger named unhandled.analyticsNo semantic convention exists. The record body follows the shape on the analytics page.

Distributed tracing

Keep the W3C traceparent propagator so a request that crosses services stays in one trace. It is the default in most SDKs. If you add other propagators, keep W3C in the list.

Unhandled also reads the OTLP span flags that mark a span's parent as remote. Recent JavaScript exporters send them. With the flags, a span with a remote parent starts a new service segment. Without them, Unhandled falls back to the span kind, and SERVER and CONSUMER spans start a segment.

Responses and errors

A successful request gets 200, encoded like the request. The 200 means the server stored the raw request. Unhandled turns it into spans, logs and metrics afterwards, so the data shows up shortly after the response. When some records are unusable, such as a span with an invalid trace or span id, the response carries an OTLP partialSuccess with the rejected count and up to 5 error messages. Unhandled stores the rest of the batch.

Errors return JSON { "code": <status>, "message": "..." }:

StatusCause
400x-unhandled-env is missing or invalid, the body is cut off or is not valid gzip, or the payload does not decode.
401x-api-key or x-unhandled-service-id is missing, or the key is invalid, revoked or expired.
403The key belongs to another service, the organization is suspended, the key's organization does not own the ingest domain you sent to, or the request Origin is not allowed for this service.
405The method is not POST or OPTIONS.
408The body did not arrive within 30 seconds.
413The body is over 10 MB, or over 16 MB after decompression.
415Unsupported content-type or content-encoding.
429The service sent more requests per second than allowed, the organization sent more bytes than its plan's rate, the organization reached its hard cap for the billing period, or the ingest queue is full. Retry after the retry-after delay.
503Storage or the API key lookup is unavailable, or the server is at its concurrent request limit. Retry after the retry-after delay.

Browsers can call the endpoints directly. The server answers preflight OPTIONS requests with 204 and allows content-type, content-encoding and the three headers above. When a service has an allowed-origins list, requests from other origins get 403. The browser SDK handles this for you.

Common questions