Self-host Telemetry

Export OpenTelemetry traces and metrics from a self-hosted instance to your own OTLP collector by setting one endpoint variable.

A self-hosted instance exports OpenTelemetry data over OTLP to a collector you run, so the traces and metrics from your deployment land in your own backend rather than anywhere else. Nothing is exported until you configure an endpoint, and that endpoint is the only thing you have to configure.

This page covers the three self-hosted postures: the self-contained compose stack, the Railway self-host template, and the bare local daemon.

1. What Your Instance Emits

Three server-side processes are instrumented, and each identifies itself to your backend by a service name:

  • dots-api — the process that serves requests.
  • dots-worker — the background worker.
  • dots-daemon — the bare local daemon.

Two signals leave the process: traces and metrics. The metric set is a fixed catalog of eight instruments, and no configuration adds to it or removes from it:

  • http.server.request.duration — server request duration, in seconds.
  • gen_ai.client.token.usage — model token usage, in tokens.
  • dots.jobs.queue.depth — queue depth, in jobs.
  • dots.jobs.workload.occupancy — workload occupancy, in slots.
  • dots.jobs.execution.duration — job execution duration, in seconds.
  • dots.outbox.pending — pending outbox rows.
  • dots.outbox.lag — outbox lag, in seconds.
  • dots.db.pool.saturation — database pool saturation, in connections.

2. Nothing Is Emitted Until You Set An Endpoint

With no endpoint configured the instance builds no exporter, no provider, and no export timer, and every traced call runs through a no-op instead. Telemetry costs an instance that has not asked for it nothing, and no data leaves a deployment you have not pointed somewhere.

An endpoint the instance cannot parse as a URL is refused rather than guessed at. A blank value, a whitespace-only value, and a host and port with no scheme all leave telemetry off. Give the variable an absolute URL carrying a scheme.

3. Turn Telemetry On With One Variable

OTEL_EXPORTER_OTLP_ENDPOINT is the switch. It is the base endpoint for every signal, and setting it alone turns on both traces and metrics:

SH
OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.example.com:4318

Set it on every process of the deployment. A worker with no endpoint drops the half of a trace that crossed the boundary into it, so an app process configured on its own records only part of any request that hands work onward.

The endpoint must be reachable from the process that exports. A bare daemon can reach a collector on the same host through loopback. A container cannot: its loopback belongs to that container, not to the host or another service.

The Self-contained Compose Stack

Put the variable in deploy/self-contained/.env, beside the compose file. Both the app and the worker service load that file, so one line configures both:

SH
# deploy/self-contained/.env
OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318

The compose file also forwards the telemetry variables by bare name from the environment it is started with, which lets a process environment supply them without writing anything into your checkout. Never commit a filled-in .env. Docker Desktop provides host.docker.internal directly. On native Linux, the committed compose file maps that same name through Docker's host-gateway value for both app and worker, so the one endpoint works on either platform.

The Railway Self-host Template

deploy/self-contained/railway.json declares the variable as optional on both the app and the worker service. Set the same externally reachable collector URL in the Railway dashboard on both, for example https://collector.example.com:4318. Neither service can reach a collector through its own loopback. The template's variable description is explicit about why both need the value: a trace crossing the app-to-worker boundary is only half recorded otherwise.

The Bare Local Daemon

The daemon reads its configuration from the environment it is started with, not from whatever happens to be exported elsewhere. Set the variable in the environment that launches it:

SH
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 dots local start

PowerShell has no inline environment prefix. From a PowerShell prompt, set the variable on its own line first:

TEXT
$env:OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318'
dots local start

A variable exported in a shell that did not launch the daemon is not a switch — the daemon composes its runtime from the environment it was handed at boot. A daemon already running keeps the environment it booted with, and [dots](/reference/glossary/dots) local start returns that running daemon untouched rather than starting a second one, so it never picks the new value up. Restart it instead:

SH
dots local restart

That stops the daemon and starts it again, reporting both outcomes, and the fresh process reads the environment you set.

4. Choose The Wire Protocol

OTEL_EXPORTER_OTLP_PROTOCOL selects the OTLP wire format. It accepts exactly two values, http/protobuf and http/json, and resolves to http/protobuf when unset:

SH
OTEL_EXPORTER_OTLP_PROTOCOL=http/json

Any other value is refused with an error naming the value it rejected and the two it accepts — grpc among them, which this instance does not speak. The comparison is exact and case-sensitive, so HTTP/JSON is refused as well. The refusal reaches only an instance that has an endpoint configured; with telemetry off the value is never read.

This refusal is harder than the one an unusable endpoint gets. An endpoint that does not parse leaves telemetry off and the instance still runs; an unsupported protocol value throws out of runtime assembly, so the process does not start at all. Check the value before you set it on a running deployment.

5. Send Traces And Metrics To Separate Destinations

Each signal takes its own endpoint, and each falls back to the base endpoint when unset:

  • OTEL_EXPORTER_OTLP_TRACES_ENDPOINT overrides the destination for traces.
  • OTEL_EXPORTER_OTLP_METRICS_ENDPOINT overrides the destination for metrics.
SH
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://metrics.internal:4318

Point either at whichever collector takes that signal; it overrides the base endpoint for that signal alone and leaves the other where it was.

One asymmetry is worth knowing before you reach for these. Setting the traces endpoint on its own turns on traces and nothing else: no metric exporter and no metric reader is built. The base endpoint is the variable that buys you both signals, which is why it is the one the section above turns telemetry on with.

6. Reach A Collector That Requires Credentials

A collector behind authentication takes its credential from OTEL_EXPORTER_OTLP_HEADERS, the one variable that adds headers to what the exporters send. Give it the OTLP key=value,key2=value2 list:

SH
OTEL_EXPORTER_OTLP_HEADERS=x-scope-orgid=dots,x-api-key=your-collector-key

The list applies to both signals, so one value covers traces and metrics whatever endpoints they go to. Leave it unset for a collector that needs no credential — the exporters then send no extra header at all.

Header values are credentials, so nothing the instance logs carries one. An entry with no = in it is dropped, and all the log says is how many entries were dropped — never a header name and never a value. A collector rejecting your traffic while the log stays quiet about the headers is the shape a mistyped list takes.

7. Name What Your Backend Sees

The exporters send to v1/traces and v1/metrics under the endpoint you set, so an endpoint of https://collector.example.com:4318 delivers to https://collector.example.com:4318/v1/traces and https://collector.example.com:4318/v1/metrics. Any collector accepting OTLP over HTTP on those paths works — nothing about the instance is specific to one backend.

Each process reports its own service name, and OTEL_SERVICE_NAME overrides the default:

SH
OTEL_SERVICE_NAME=dots-api-eu

Set it per process. Setting it in an environment two processes share collapses both onto one name, and the API process and the worker stop being distinguishable at your backend.

8. What Never Leaves Your Instance

No raw prompt, response, image, audio, document, or query text is recorded in a span — on any posture, under any configuration. Spans carry the shape of the work, never its content.

Metrics are bounded the same way. The catalog is closed and its attributes come from a fixed allowlist, so a metric carries neither a company id nor free text, and no metric grows an unbounded attribute set.

9. Confirm It Is Working

The instance records nothing when telemetry succeeds. There is no success line in the log, no health field, and no command reporting telemetry state, so you confirm it at your own collector:

  1. Look for the service name of the process you configured — dots-api, dots-worker, or dots-daemon.
  2. Look at the v1/traces and v1/metrics paths under the endpoint you set. Traffic on those paths is the instance exporting.
  3. Read the process log for the failures the instance does report.

Two of those log records answer most of what goes wrong. unusable OTLP endpoint; telemetry disabled means the endpoint did not parse as a URL and telemetry stayed off. failed to flush traces and failed to flush metrics mean the exporter built and the collector did not take the data.

A rejected protocol value fails loudly rather than quietly, naming the value it rejected — which is itself proof the variable was read. Silence across all of these, plus data arriving at your collector, is the whole confirmation.

Where To Go Next

Self-host Dots is the full self-hosting reference: every posture, the environment file, and the bring-your-own-infrastructure paths. Quickstart: Self-hosting Dots is the short route to an instance running before you configure any of this.

Self-host Telemetry · This page in the graph

Connected Records7

Documents
packages/local-runtime/src/daemon/boot.ts
packages/observability/src/internal/tracing.ts
Mentions
References
7 connections.

Where To Go Next

Reading this as an agent?For Agents
Something wrong on this page?Dots On GitHub