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:
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:
# 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:
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:
$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:
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:
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_ENDPOINToverrides the destination for traces.OTEL_EXPORTER_OTLP_METRICS_ENDPOINToverrides the destination for metrics.
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:
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:
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:
- Look for the service name of the process you configured —
dots-api,dots-worker, ordots-daemon. - Look at the
v1/tracesandv1/metricspaths under the endpoint you set. Traffic on those paths is the instance exporting. - 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
- documentstopackages/local-runtime/src/daemon/boot.ts
- documentstopackages/observability/src/internal/tracing.ts
- mentionstoDIIICE: Data
- mentionstoDots
- referencestoQuickstart: Self-hosting Dots
- referencestoSelf-host Dots
- referencesfromSelf-host Dots