OpenTelemetry Collector debug exporter: stop guessing
A practical guide to using the Collector debug exporter to prove where traces, metrics, and logs enter or disappear in a pipeline.
The worst OpenTelemetry failures are quiet.
The service says it emitted spans. The Collector is running. The backend token came from the right secret. The dashboard is empty.
This is the point where teams start editing YAML like they are trying to get lucky. Change the endpoint. Restart the Collector. Rename a pipeline. Toggle HTTP and gRPC. Switch JSON and protobuf. Try again.
The better question is smaller: did the telemetry reach this point in the Collector pipeline?
The debug exporter exists for that question. It prints telemetry from a pipeline to the Collector output. It is not glamorous. It is not a backend. It is a flashlight.
What it proves
A Collector pipeline is simple on paper:
receiver -> processors -> exporters
In practice, telemetry can disappear in several places:
| Failure area | Common cause |
|---|---|
| Application | SDK disabled, wrong protocol, process exits before flush |
| Network | Wrong host, wrong port, service discovery issue |
| Receiver | Client sends HTTP to a gRPC endpoint or the reverse |
| Processor | Filter, transform, memory limiter, or routing drops data |
| Exporter | Wrong token, path, dataset, TLS, encoding, or backend endpoint |
The debug exporter cuts the search space. If it prints data at the end of the pipeline, the receiver and earlier processors are alive. If it prints nothing, stop blaming the backend and move upstream.
That one distinction saves hours.
Minimal config
Start with the smallest useful test. One receiver. One batch processor. One debug exporter. No routing. No clever transforms. No backend token.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
The pipeline is the truth. Defining exporters.debug does nothing unless a service.pipelines.*.exporters list actually references it.
That catches people all the time. The config can parse, the Collector can start, and the exporter can still be sitting unused.
Verbosity levels
The exporter has three useful modes:
| Verbosity | Use it when | What to expect |
|---|---|---|
basic |
You only need to prove data arrived | Counts and compact output |
normal |
You need enough shape to inspect resources and attributes | More detail without dumping everything |
detailed |
You need to inspect actual records | Very noisy output |
Use basic first. Move to normal when you need resource attributes. Use detailed briefly and deliberately.
Detailed output can include sensitive data. If your spans or logs carry headers, URLs, SQL statements, prompts, or customer fields, the debug exporter may print them. Do not leave it on in production because “it helped once.”
Read from the bottom
When debugging Collector config, read from service.pipelines upward.
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
That tells you the actual path. The component definitions only say what could exist. The pipeline says what runs.
Component names are literal. If you define this:
exporters:
debug/detailed:
verbosity: detailed
but reference this:
exporters: [debug]
then you are not using the debug/detailed instance. Close enough is still wrong.
The old logging exporter
You may see older examples using the logging exporter. Treat that as legacy config. Modern Collector configs should use debug.
This is an easy upgrade footgun because both names sound like they should print telemetry. The component name your Collector accepts depends on the distribution and version you are running.
If a config works locally and fails in an internal hardened Collector image, check the binary too. A custom Collector only has the components compiled into it. Sometimes the YAML is fine and the binary simply does not include the exporter.
Use it as a probe
The debug exporter is most useful when you place it deliberately.
If you want to know whether the receiver gets spans:
service:
pipelines:
traces:
receivers: [otlp]
processors: []
exporters: [debug]
If you want to know whether a processor drops spans, create two temporary Collector configs or two test pipelines: one before the processor and one after. Keep the experiment boring.
If you want to know whether the backend exporter is the problem, send the same pipeline to both exporters:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp/backend]
If debug prints records and the backend stays empty, the backend export path is the suspect. If debug prints nothing, stay upstream.
What to look for
When output appears, look for the boring facts first:
- Is the signal type the one you expected?
- Does
service.nameexist? - Are resource attributes present?
- Are span names reasonable?
- Are log bodies present?
- Are metric names and temporality what you expected?
- Did a processor remove a field you needed?
Do not inspect everything. Ask the smallest question that moves the investigation forward.
Safe debugging rules
The debug exporter is a tool for humans, not a production export path.
Use these rules:
| Rule | Why |
|---|---|
| Keep it temporary | Output volume can get noisy fast |
Use basic first |
It proves flow without dumping every field |
| Avoid long-running production use | Sensitive fields may print to logs |
| Pair it with a narrow test | One service and one request is easier to reason about |
| Remove it after debugging | A debug path should not become architecture |
If you need permanent inspection, build a real telemetry validation path. The debug exporter is for answering “did it get here?” quickly.
Conclusion
The debug exporter is one of the fastest ways to stop guessing in OpenTelemetry.
It turns a vague blank-dashboard problem into a pipeline question. Did data reach the Collector? Did it survive the processors? Did it leave toward the backend?
Use it as a probe. Keep the config small. Read the pipeline from the bottom. Start with basic verbosity. Remove it when the question is answered.
The point is not to print telemetry forever. The point is to make the next move obvious.