OpenTelemetry
Overview

OpenTelemetry Integration

OpenTelemetry over OTLP/HTTP is how you send data to TracePath. Go, Node.js, Python, PHP, Java, .NET, Ruby: whatever your server runs on, it exports traces, metrics, and logs to the same ingest endpoint. There is no TracePath SDK to install, no vendor wrapper to learn per framework, and no separate TracePath project per backend service.

OpenTelemetry (opens in a new tab) (OTel) is the industry-standard, vendor-neutral framework for collecting telemetry. If your app already uses OTel, you can point it at TracePath by changing two settings: the endpoint and one header. If you are starting fresh, any OTel SDK will work.

Three things you need before any snippet on this page runs:

  1. An account. Sign up at app.tracepath.dev/register (opens in a new tab).
  2. A project. Create one with framework OpenTelemetry. Send your API, background workers, scheduled jobs, AI calls, and host metrics to that one project — see Project Structure.
  3. Its project token. Copy it from the project's Connection page in the dashboard. It is the only credential the ingest endpoint accepts.

How It Works

  1. Instrument your app with an OpenTelemetry SDK (or auto-instrumentation).
  2. Export via OTLP/HTTP to https://ingest.tracepath.dev/api/otel, with Authorization: Bearer <project_token>. Your SDK, or an OTel Collector in front of it, does the sending.
  3. TracePath maps the data: spans become endpoints and traces, metrics appear on your dashboards, and logs are indexed and linked to their originating traces.

Three signals, three exporters

OTel treats traces, metrics and logs as three independent signals. Each one needs its own exporter wired into the SDK. Configuring a trace exporter does not start sending logs, and auto-instrumentation packages do not add a log exporter for you. This is the most common reason the Logs page stays empty.

SignalPathWhat you must addWithout it
Traces/v1/tracesa trace exporter (traceExporter / BatchSpanProcessor)No endpoints, tasks, spans or issues
Metrics/v1/metricsa metric reader (PeriodicExportingMetricReader)Empty dashboard widgets
Logs/v1/logsa log record processor and a bridge from your logging libraryEmpty Logs page

Logs need one extra step the other two do not. The OTel log exporter only ships records emitted through the OTel logs API, so you also need a bridge from whatever you actually log with: LoggingHandler for Python's logging, an slog handler for Go, a Monolog handler for PHP, the built-in appenders for the Java agent. A plain console.log or print sends nothing.

All three signals go to the same host and use the same project token. See Logs for the per-language wiring.

Supported Languages

Any language with an OTel SDK can export to TracePath. Here are the most common ones:

LanguageOTel SDKInstall
JavaOpenTelemetry Java (opens in a new tab)The 2.x Java agent JAR (opens in a new tab), no code changes (see the Spring Boot quick start below)
PythonOpenTelemetry Python (opens in a new tab)pip install opentelemetry-distro opentelemetry-exporter-otlp, then opentelemetry-bootstrap -a install, then run under opentelemetry-instrument. See Python guide for FastAPI, Flask, and any WSGI/ASGI app
Python / DjangoOpenTelemetry Python zero-code (opens in a new tab)pip install opentelemetry-distro opentelemetry-exporter-otlp opentelemetry-instrumentation-django. See Django guide
C# / .NETOpenTelemetry .NET (opens in a new tab)dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol (pulls in OpenTelemetry)
GoOpenTelemetry Go (opens in a new tab)go get go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Node.jsOpenTelemetry JS (opens in a new tab)npm install @opentelemetry/sdk-node. See Node.js guide, NestJS guide, Hono guide, Next.js guide
PHP / SymfonyOpenTelemetry PHP (opens in a new tab)composer require open-telemetry/sdk open-telemetry/exporter-otlp, plus the opentelemetry PECL extension for zero-code instrumentation. See Symfony guide
PHP / Laravelkeepsuit/laravel-opentelemetry (opens in a new tab)composer require keepsuit/laravel-opentelemetry. See Laravel guide
Cloudflare WorkersWorkers OTel export (opens in a new tab)Built-in, traces and logs only, Workers Paid. See Cloudflare guide

Every package named above is a public OpenTelemetry package, not a TracePath one. TracePath publishes no SDK, no wrapper and no CDN script: the OTLP endpoint below is the whole integration surface, which is also why an app already exporting OTel needs no new dependency at all.

Every OTel ecosystem ships the API, the SDK and the OTLP exporter as separate packages. If you install only the SDK, you get an app that records spans and throws them away. The install commands above cover the exporter too.

Configuration

SettingValue
Endpointhttps://ingest.tracepath.dev/api/otel
Traces path/v1/traces
Metrics path/v1/metrics
Logs path/v1/logs
Auth headerAuthorization: Bearer <project_token>
ProtocolOTLP/HTTP only, Protobuf or JSON. gRPC is not supported, so OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf is required
CompressionGzip supported (Content-Encoding: gzip)
Content-Typeapplication/x-protobuf (or application/protobuf) for OTLP/Protobuf. Anything else, including a missing header, is parsed as OTLP/JSON. The response comes back in the same encoding you sent
Max body size10 MB, applied to the raw body and to the decompressed output. Gzip does not raise it. A larger export is rejected with 413
Auth failure401 with an empty body. The header value must literally start with Bearer

https://ingest.tracepath.dev is for telemetry only. The dashboard, the management API and source-map uploads live on https://app.tracepath.dev; point your exporter at the ingest host and nothing else.

TracePath serves OTLP over HTTP only. There is no gRPC endpoint, so set your exporter's protocol to http/protobuf (or http/json) explicitly. Several SDKs default to gRPC when the variable is unset: with OTEL_TRACES_EXPORTER=otlp and no OTEL_EXPORTER_OTLP_PROTOCOL, the Python SDK resolves to gRPC, and the Java agent does the same. The failure is silent. The app runs, exits 0, prints no warning, and every export lands nowhere.

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf

Set that wherever you configure the exporter. The one exception is PHP, which needs http/json instead.

An export larger than 10 MB after decompression is rejected outright with 413 Request Entity Too Large and {"error":"request body exceeds the 10MB limit"}. Nothing is truncated and nothing is partially ingested. OTLP exporters treat 4xx as permanent and drop the batch instead of retrying, so that data is lost. Keep batches comfortably under the limit by capping the exporter's batch size: maxExportBatchSize in the JS SDK, max_export_batch_size in Python, send_batch_max_size on the Collector's batch processor. The SDK default of 512 spans per export is far below the limit.

When ingest answers 503

503 is the one rejection you are meant to retry, and every OTLP exporter and the Collector already do. It has three causes, told apart by the body:

BodyMeaningWhat to do
{"error":"ingest saturated, retry later"}, Retry-After: 2A short burst hit the concurrency limit.Nothing. The exporter resends within seconds.
{"error":"Monthly ingest quota exceeded for this plan. Upgrade or wait for the next period."}, Retry-After: 60The organization has used its plan's monthly ingest allowance.Upgrade, or wait for the next calendar month. Owners were emailed at 80%.
{"error":"This organization is suspended. Ingest is paused."}, Retry-After: 60The organization is suspended — non-payment, or an acceptable-use problem.Write to [email protected]. Retrying will not clear it.

The first is normal backpressure and resolves itself. The other two do not: the exporter will keep retrying and backing off for as long as the pause lasts, and anything that outlives its queue is lost.

Your plan's monthly ingest allowance — 5 GB on Free, 50 GB on Pro, 250 GB on Business — is counted as decompressed bytes per organization per calendar month, across OTLP and every other ingest path. There is no overage billing, so a runaway exporter pauses ingest rather than running up a bill. See Billing and plans.

Quick Start: Direct SDK Export

The simplest setup: your app exports directly to TracePath, with no extra infrastructure.

Here is a complete Node.js setup that wires all three signals. Install the packages first:

npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node \
  @opentelemetry/instrumentation @opentelemetry/api \
  @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/exporter-logs-otlp-http @opentelemetry/sdk-metrics \
  @opentelemetry/sdk-logs @opentelemetry/resources @opentelemetry/api-logs

@opentelemetry/instrumentation is listed because the loader hook below is loaded by path from it, and @opentelemetry/api because your own code imports it. Both also arrive as transitive dependencies of the packages above, so npm's flat node_modules resolves them even when they are not declared. Declare them anyway, since your code names them directly.

Then create telemetry.mjs:

// telemetry.mjs
// Load it before your app: node --import ./telemetry.mjs server.js
import { register } from "node:module";
register("@opentelemetry/instrumentation/hook.mjs", import.meta.url);
 
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
 
const BASE = "https://ingest.tracepath.dev/api/otel";
const headers = { Authorization: "Bearer your-project-token" };
 
const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    "service.name": "my-service",
    "service.version": "1.0.0",
  }),
  traceExporter: new OTLPTraceExporter({ url: `${BASE}/v1/traces`, headers }),
  metricReaders: [
    new PeriodicExportingMetricReader({
      exporter: new OTLPMetricExporter({ url: `${BASE}/v1/metrics`, headers }),
      exportIntervalMillis: 30000,
    }),
  ],
  logRecordProcessors: [
    new BatchLogRecordProcessor({
      exporter: new OTLPLogExporter({ url: `${BASE}/v1/logs`, headers }),
    }),
  ],
  instrumentations: [getNodeAutoInstrumentations()],
});
 
sdk.start();

Start your app with node --import ./telemetry.mjs server.js. Loading the file first is what lets auto-instrumentation patch your HTTP server and database clients before they are imported.

Four details that break this silently if you get them wrong:

  • The two node:module lines at the top are required for an ESM app (.mjs, or "type": "module"). Without the hook, no framework is patched, http.route is never set, and your endpoints are named after raw span names. A CommonJS app (require, .cjs) does not need them.
  • resourceFromAttributes replaced the old new Resource(...), which was removed in @opentelemetry/resources 2.x. The old form throws at import time.
  • BatchLogRecordProcessor takes an options object, not a bare exporter. new BatchLogRecordProcessor(exporter) constructs fine and then exports nothing, with no error.
  • metricReaders is the current option. The singular metricReader still works but is deprecated.

The same pattern applies to any language: set the OTLP/HTTP endpoint and the Authorization header in your exporter config, once per signal.

End to End: One Request, Its Spans, and Its Logs

Traces and logs are separate signals with separate exporters, but they meet again in the dashboard. A log emitted inside an active span carries that span's trace_id, and the Endpoint detail page uses that id to show the request's log lines next to its waterfall.

Here is the request side, using the telemetry.mjs above. Auto-instrumentation produces spans like these for you; they are written out by hand so the correlation is visible.

import { trace, SpanKind } from "@opentelemetry/api";
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
 
const tracer = trace.getTracer("shop");
const logger = logs.getLogger("shop");
 
await tracer.startActiveSpan(
  "GET /orders/:id",
  {
    kind: SpanKind.SERVER,
    attributes: { "http.request.method": "GET", "http.route": "/orders/:id" },
  },
  async (serverSpan) => {
    logger.emit({
      severityNumber: SeverityNumber.INFO,
      severityText: "INFO",
      body: "handling order lookup",
      attributes: { "order.id": "ord_123" },
    });
 
    await tracer.startActiveSpan(
      "SELECT orders",
      {
        kind: SpanKind.CLIENT,
        attributes: {
          "db.system": "postgresql",
          "db.query.text": "SELECT * FROM orders WHERE id = $1",
        },
      },
      async (dbSpan) => {
        // ... run the query ...
        dbSpan.end();
      },
    );
 
    serverSpan.setAttribute("http.response.status_code", 200);
    serverSpan.end();
  },
);

What lands in TracePath:

What you wroteWhere it shows up
The root SERVER span with http.routeEndpoints, as GET /orders/:id, with its duration and status
The child CLIENT spanThe Spans waterfall on that endpoint's detail page, labelled with the SQL from db.query.text
The logger.emit callThe Logs card on the same page, This Trace tab
Anything you recordException on either spanIssues, linked back to this endpoint

The glue is that the log record carries the root span's trace_id, and a root span's Endpoint row is keyed by that same trace id.

Note the SpanKind.SERVER on the outer span. With the default INTERNAL kind and no HTTP attributes, the span is dropped and none of this appears. See Traces for the full classification rules.

Quick Start: Spring Boot (Java Agent)

The OpenTelemetry Java agent (opens in a new tab) instruments Spring Boot applications with zero code changes. Download opentelemetry-javaagent.jar from the OpenTelemetry project's latest release (the download link is on that page), use the 2.x agent, and pass it to the JVM:

java \
  -javaagent:opentelemetry-javaagent.jar \
  -Dotel.service.name=my-spring-app \
  -Dotel.exporter.otlp.protocol=http/protobuf \
  -Dotel.exporter.otlp.endpoint=https://ingest.tracepath.dev/api/otel \
  -Dotel.exporter.otlp.headers="Authorization=Bearer <project_token>" \
  -jar target/my-app.jar

-Dotel.exporter.otlp.endpoint takes the base URL. The agent appends /v1/traces, /v1/metrics and /v1/logs itself, which is exactly how TracePath's ingest paths are laid out. (The per-signal settings such as otel.exporter.otlp.traces.endpoint are used as-is, so those need the full path.) -Dotel.service.name becomes the Server Name on every endpoint, task and issue.

Set -Dotel.exporter.otlp.protocol=http/protobuf explicitly even though it is the default on the 2.x agent. TracePath speaks OTLP/HTTP only. A 1.x agent JAR, or any environment that already exports OTEL_EXPORTER_OTLP_PROTOCOL=grpc, will send telemetry to a port that does not exist, and nothing in the dashboard will tell you.

All three signals are exported by this command. otel.traces.exporter, otel.metrics.exporter and otel.logs.exporter all default to otlp, so JVM and HTTP server metrics land on your dashboards and your Logback or Log4j output lands in Logs, with no extra flags and no appender to register. If you deliberately want traces only, add -Dotel.metrics.exporter=none -Dotel.logs.exporter=none.

Leave otel.exporter.otlp.metrics.default.histogram.aggregation at its default EXPLICIT_BUCKET_HISTOGRAM. TracePath reads explicit-bucket histograms and exposes each one as <name>.avg and <name>.count. Base-2 exponential histograms are not read and are dropped without an error, so http.server.request.duration and the JVM latency histograms would silently disappear from your dashboards.

In a container or a Kubernetes manifest, use the environment-variable form instead. Same settings, uppercased, with . and - replaced by _:

JAVA_TOOL_OPTIONS=-javaagent:/app/opentelemetry-javaagent.jar
OTEL_SERVICE_NAME=my-spring-app
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.tracepath.dev/api/otel
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <project_token>

Do not quote the OTEL_EXPORTER_OTLP_HEADERS value in a Kubernetes manifest. The quotes become part of the header value and the request comes back 401.

What lands where, with the command above:

Spring BootTracePath
Controller requests (@GetMapping("/orders/{id}"))Endpoints, named GET /orders/{id}, because the Spring Web MVC instrumentation sets http.route
@Scheduled jobs, @Async work, queue consumersTasks
JDBC, HTTP client and Redis calls inside a requestChild spans on the trace detail page
Uncaught controller exceptionsIssues, with the full JVM stack trace
Logback / Log4j outputLogs, correlated to the trace that produced it
JVM and HTTP server metrics (jvm.memory.used, http.server.request.duration)Metrics and dashboard widgets. Histograms appear as <name>.avg and <name>.count

Exceptions that reach the servlet container are recorded as span events and show up in Issues. TracePath groups them by the exception class plus the normalized frame list. The message is stripped and Java, Kotlin and Scala line numbers are removed, so one failure keeps a single issue across redeploys and across differing error messages. A different call site, or a frame added or removed in the same path, is a different issue by design.

Two caveats worth knowing. The message is stripped only from the first line of the header, and only when the exception class name contains no $. Exceptions from nested classes, and exceptions whose message spans several lines, can therefore split into one issue per distinct message.

Quick Start: OTel Collector

If you already run an OpenTelemetry Collector (opens in a new tab) or want a central pipeline that fans out to multiple backends, you can route data through it. This is optional. The direct SDK export above works without a Collector.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
 
processors:
  batch:
 
exporters:
  otlphttp:
    endpoint: "https://ingest.tracepath.dev/api/otel"
    headers:
      Authorization: "Bearer your-project-token"
 
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp]

Every pipeline must declare at least one receiver. A pipeline with only an exporters: line fails validation with must have at least one receiver and the Collector never starts.

The otlphttp exporter appends /v1/traces, /v1/metrics and /v1/logs to the base endpoint, so point it at /api/otel and nothing else.

Your apps then send to the Collector on localhost:4318 (OTLP/HTTP) or localhost:4317 (OTLP/gRPC), and the Collector is the only thing that needs your TracePath token.

Nothing Is Showing Up

TracePath accepts telemetry it cannot use rather than rejecting the whole batch, so a misconfiguration usually looks like silence rather than an error. Work down this list.

Check the wire first. Post an empty span batch and watch the status code:

curl -i -X POST https://ingest.tracepath.dev/api/otel/v1/traces \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <project_token>' \
  -d '{"resourceSpans":[]}'
  • 200 with the body {} means the endpoint and token are good. The problem is in your SDK, or in how your spans are shaped.
  • 401 with an empty body means a bad token, or a header that does not start with Bearer . Check you copied the project token from the Connection page and not a personal access token.
  • 503 means a saturation burst, a used-up monthly allowance or a suspended organization — the body tells you which. See When ingest answers 503.
  • 413 means the batch exceeded 10 MB after decompression. Nothing was ingested. Lower the exporter's batch size.
  • 404 means the path is wrong. The three full paths are /api/otel/v1/traces, /api/otel/v1/metrics and /api/otel/v1/logs — there is no version segment before otel, so /api/v1/traces and /otel/v1/traces both 404.
  • Anything that is not JSON means you are talking to the wrong host. OTLP goes to ingest.tracepath.dev; app.tracepath.dev serves the dashboard and will answer with HTML.

Then check the shape.

SymptomCauseFix
Background job or cron never appears on TasksIts root span uses the default SpanKind.INTERNAL, so it is discardedSet kind: SpanKind.CONSUMER, or add a console.command attribute for CLI commands
Endpoint is named after the span, not the routeNo http.request.method or http.method on the span. Without a method the route is ignored entirelySet the method attribute alongside http.route
One endpoint row per URL, thousands of themOnly url.path is set, so the concrete URL becomes the nameSet http.route to the low-cardinality template, /users/:id
Requests appear as UNMATCHEDThey are 404s with no matched route, collapsed on purposeExpected. A matched route returning 404 keeps its name
The same route is listed twice with a Mixed chipA batch processor split the parent and child spans across two exports, so the child was promoted to its own endpointRaise maxExportBatchSize / scheduledDelayMillis, or stop emitting the redundant sub-handler span
Nothing at all from an ESM Node appThe @opentelemetry/instrumentation/hook.mjs loader hook is missing, so nothing was patchedAdd the two node:module lines shown above
Logs page is emptyNo log exporter is wired (a trace exporter does not send logs), or there is no bridge from your logging library into the OTel logs APISee Logs
Logs exist but the endpoint's Logs card is emptyThe endpoint was promoted from a non-root span, so it is keyed by span id and not by the trace id the logs carrySearch the trace id on the Logs page
A whole metric family is missingIt is an ExponentialHistogram or a Summary. Both are dropped without an errorSwitch to explicit-bucket histograms, or convert in the Collector
Metrics arrive but every series looks identicalThe distinguishing attribute is on the Resource and is not in the allowlistEmit it as a data-point attribute instead. See Metrics
Large exports vanish and the exporter logs a 413The batch exceeded 10 MB after decompression and was rejected outrightLower the exporter's batch size

Turn on the SDK's own diagnostics. Most silent client-side failures, including a mis-constructed exporter or processor, only surface here:

import { diag, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api";
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

The equivalents are OTEL_LOG_LEVEL=debug for Python and the Java agent, and otel.SetErrorHandler for Go.

Connection Page

Every project in the dashboard at app.tracepath.dev (opens in a new tab) has a Connection page with a ready-made config snippet and that project's token already filled in. Open Connection in the sidebar and copy from there rather than retyping the endpoint.

Connection page

Next Steps

  • Traces: how OTel spans map to TracePath concepts
  • Metrics: supported metric types and histogram handling
  • Logs: export OTel logs and link them to your traces

Framework guides, all on this same OTLP path:

Not listed here? There is nothing framework-specific to install. Follow your language's OTel quick start, then set the endpoint and the Authorization header from the Configuration table above.

Something not working, or a question this page does not answer: [email protected].