Skip to content

feat(tracer): Implement OTLP Traces Export#4600

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 68 commits into
mainfrom
mtoff/otlp-export-traces
Apr 15, 2026
Merged

feat(tracer): Implement OTLP Traces Export#4600
gh-worker-dd-mergequeue-cf854d[bot] merged 68 commits into
mainfrom
mtoff/otlp-export-traces

Conversation

@mtoffl01

@mtoffl01 mtoffl01 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Implements the OTLP trace export pipeline (See: RFC) for dd-trace-go: when OTEL_TRACES_EXPORTER=otlp is set, the tracer converts Datadog spans into OTLP protobuf format and sends them directly to an OpenTelemetry Collector instead of the Datadog agent.

This adds three new components:

  1. span_to_otlp.go — converts internal Datadog spans to OTLP TracesData protobuf, including resource attributes, span kind/status, events, links, 128-bit trace IDs, and sampling filtering (only sampled spans are exported).
  2. otlp_writer.go — a traceWriter implementation that buffers converted OTLP spans, marshals them to protobuf, and flushes with retry logic.
  3. otlp_transport.go — a lightweight HTTP transport for sending protobuf payloads to an OTLP endpoint, separate from the existing Datadog-protocol transport.

The PR also renames the existing transport interface and config.transport field to ddTransport to make it clear that it handles Datadog-specific traffic (msgpack traces, stats), and ensures ddTransport always points at the Datadog agent even when OTLP mode is active.

Motivation

We are adding native OTLP export support to dd-trace-go so users can send traces directly to any OTLP-compatible collector. A prior PR added the configuration layer that resolves OTEL_TRACES_EXPORTER, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, and OTEL_EXPORTER_OTLP_TRACES_HEADERS. This PR builds the runtime pipeline on top of that configuration.

IMPORTANT NOTE!

The existing agentTraceWriter and ddTransport was too tightly coupled to Datadog conventions — msgpack encoding, agent sampling rate feedback, stats collection — to extend it for the otlp path, so a separate writer and transport were introduced instead. The otlpTraceWriter owns its own otlpTransport directly (rather than reaching through config.ddTransport), keeping OTLP concerns isolated from the Datadog agent path. A follow-up PR may remove ddTransport from the config struct entirely, instead passing the transport directly to each writer and consumer at construction time. This would make all writer implementations structurally consistent — each owning its transport as a field — and eliminate the implicit coupling through config.

An additional PR to disable stats collection in otlp mode is in progress here.

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • System-Tests covering this feature have been added and enabled with the va.b.c-dev version tag.
  • There is a benchmark for any new code, or changes to existing code.
  • If this interacts with the agent in a new way, a system test has been added.
  • New code is free of linting errors. You can check this by running make lint locally.
  • New code doesn't break existing tests. You can check this by running make test locally.
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • All generated files are up to date. You can check this by running make generate locally.
  • Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running make fix-modules locally.

Unsure? Have a question? Request a review!

mtoffl01 and others added 30 commits March 23, 2026 09:30
… service name differs from global service name
…me (#4576)

### What does this PR do?

Moves the `strconv.ParseInt` call for `_dd.p.dm` (decision maker) out of the v1 payload flush path and into the write path.

A `dm uint32` field is added to the `trace` struct. It is kept in sync with `propagatingTags[keyDecisionMaker]` at all mutation sites (`setPropagatingTagLocked`, `unsetPropagatingTagLocked`, `replacePropagatingTags`). A shared `unsetPropagatingTagLocked` helper is introduced so the delete-and-clear logic is not duplicated between `unsetPropagatingTag` and `setSamplingPriorityLockedWithForce`.

`payloadV1.push` now reads the pre-computed value via `trace.decisionMaker()` instead of fetching the string from the propagating tags map and parsing it on every flush.

### Motivation

`payloadV1.push` is called once per finished trace chunk. On every call it was acquiring a read lock, doing a map lookup, and then running `strconv.ParseInt` — all to read a value that was set at most once or twice during the lifetime of the trace. Moving the parse to write-time eliminates the per-flush map lookup and parse, replacing them with a single scalar read behind an `RLock`.

### Reviewer's Checklist

- [x] Changed code has unit tests for its functionality at or near 100% coverage.
- [x] New code is free of linting errors. You can check this by running `make lint` locally.
- [x] New code doesn't break existing tests. You can check this by running `make test` locally.
- [x] Add an appropriate team label so this PR gets put in the right place for the release notes.
- [x] All generated files are up to date. You can check this by running `make generate` locally.


Co-authored-by: dario.castane <[email protected]>
…ED` once, use a global `bool` (#4548)

Stop checking environment variable `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` on span start. Checks it once, use a global `bool`.

It'd be better to load it on `newConfig` and make the value available, but this one happens in `newSpanContext`, which modification will cause a lot of changes in multiple files, including tests.

Complete #1905 work using the current benchmarking platform.

- [x] Changed code has unit tests for its functionality at or near 100% coverage.
- [x] There is a benchmark for any new code, or changes to existing code.
- [x] New code is free of linting errors. You can check this by running `make lint` locally.
- [x] New code doesn't break existing tests. You can check this by running `make test` locally.
- [x] Add an appropriate team label so this PR gets put in the right place for the release notes.

Unsure? Have a question? Request a review!

Co-authored-by: kakkoyun <[email protected]>
Co-authored-by: kemal.akkoyun <[email protected]>
Co-authored-by: Benjamin De Bernardi <[email protected]>
Comment thread ddtrace/tracer/option.go
// resolveTraceTransport returns the trace URL and headers for the transport
// based on whether OTLP export mode is active.
// resolveTraceTransport returns the trace URL and headers for the Datadog
// agent transport. In OTLP export mode the ddTransport is not used for trace

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread ddtrace/tracer/otlp_transport.go
Comment thread ddtrace/tracer/otlp_writer.go
@mtoffl01
mtoffl01 requested review from hannahkm and kakkoyun April 13, 2026 17:57
@darccio

darccio commented Apr 14, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d6156e0db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ddtrace/tracer/span_to_otlp.go
Comment thread ddtrace/tracer/span_to_otlp.go Outdated
Comment on lines +58 to +71
return &otlptrace.Span{
TraceId: convertTraceID(s.context.traceID.Upper(), s.context.traceID.Lower()),
SpanId: convertSpanID(s.spanID),
ParentSpanId: convertParentSpanID(s.parentID),
Name: s.resource,
Kind: convertSpanKind(getSpanKind(s)),
StartTimeUnixNano: uint64(s.start),
EndTimeUnixNano: uint64(s.start + s.duration),
Attributes: convertSpanAttributes(s, defaultServiceName),
Events: convertEvents(s),
Links: convertSpanLinks(s.spanLinks),
Status: convertSpanStatus(s),
TraceState: convertTraceState(s.context),
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could make this more efficient by wrapping *tracer.Span with otlptrace.Span and having getters returning each expected field in the converted format.

This can be done in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every field converted here is a required field in the protobuf output; there are no unused conversions. So as I see it, deferring the conversion would just change when the latency is introduced, not how much latency we introduce. Unless I am missing something?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking on the heap allocation that happens here. If we had a wrapper type that stores a pointer to the tracer.Span we could probably make this function inlineable, as we wouldn't need to run all these conversion functions here.

I'm also not sure if these conversion functions are inlining here.

In any case, looking how this is used, I see it's not possible to optimize that much.

Comment thread ddtrace/tracer/span_to_otlp.go Outdated
Comment thread ddtrace/tracer/span_to_otlp.go
Comment thread ddtrace/tracer/tracer_test.go
@mtoffl01
mtoffl01 requested a review from darccio April 14, 2026 17:56

@hannahkm hannahkm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! 🎉

@kakkoyun kakkoyun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Thanks for enduring all the reviews. It's an critical piece and a bi g PR. I hope we haven't missed anything. We can always iterate.

scope *otlpcommon.InstrumentationScope
spans []*otlptrace.Span // +checklocks:mu
buffSize int // +checklocks:mu
baseSize int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Lack of this annotation should not be green on the CI, I need to check this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants