Skip to content

feat(otlp/export): offline OTLP trace/metric/log export#4994

Draft
khanayan123 wants to merge 3 commits into
ayan.khan/llmobs-explicit-span-idsfrom
ayan.khan/otlp-export
Draft

feat(otlp/export): offline OTLP trace/metric/log export#4994
khanayan123 wants to merge 3 commits into
ayan.khan/llmobs-explicit-span-idsfrom
ayan.khan/otlp-export

Conversation

@khanayan123

@khanayan123 khanayan123 commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Adds a public otlp/export package: offline clients that POST already-built OTLP collector-proto requests as raw protobuf to Datadog's agentless OTLP intake or a caller-provided collector/Agent endpoint.

c, _ := export.NewTraceClient(export.Config{Site: "datadoghq.com", APIKey: key})
res, err := c.ExportTraces(ctx, []*tracepb.ExportTraceServiceRequest{req})
// also NewMetricClient/ExportMetrics and NewLogClient/ExportLogs

Stacked on #4936 (llmobs/export). Base branch is ayan.khan/llmobs-explicit-span-ids, so this diff shows only the OTLP additions plus the shared internal/exportutil retry helper. This branch already contains all of #4936's commits — review/merge #4936 first.

The OTLP half of the RFC "Raw LLM Observability Export, Multi-Destination Routing, and Caller-Assigned IDs in dd-trace-go."

Design

  • Accepts go.opentelemetry.io/proto/otlp/collector/{trace,metrics,logs}/v1 Export*ServiceRequest values (no new deps — proto/otlp + protobuf are already direct deps).
  • Atomic per request: one *Export*ServiceRequest → one POST → one indexed RequestResult. The SDK never merges or splits requests.
  • Routes: Datadog (Site+APIKeyhttps://otlp.<site>/v1/<signal> with dd-api-key) or collector/Agent (Endpoint, no auth). Metrics on the Datadog route add dd-otel-metric-config: {"histograms":{"mode":"distributions"}} (metrics-only, Datadog-route-only).
  • OTLP/HTTP-spec retry (default 3 attempts): only 429/502/503/504 (and network errors) retry; every other 4xx/5xx — including 408/500/501 — is permanent; a caller-cancelled context is not retriable. Honors Retry-After (delta-seconds or HTTP-date, clamped to 60s) on throttling responses instead of the default backoff.
  • Partial success surfaced: a 2xx whose OTLP response reports rejected_{spans,data_points,log_records} > 0 is reported as a failed request (a data-loss signal, not silent success). A failed read of a 2xx body is likewise surfaced as a retryable failure rather than reported delivered.
  • Readable errors: failed responses decode the google.rpc.Status message into RequestResult.ResponseSnippet (bounded/UTF-8-safe via exportutil.Snippet) instead of raw protobuf control bytes.
  • Caller trace IDs/flags/tracestate are preserved as-is (the SDK only marshals). Callers own proto construction, projection, temporality/histogram semantics, dedup, and durable retry.
  • Raw-proto transport that coexists with the OTel-SDK-based exporters in ddtrace/opentelemetry — it does not replace them. Retry mechanics live in a shared internal/exportutil.Retry with a pluggable classifier; llmobs/export keeps its own broader classification via the internal LLM Obs transport.

Tests

go test -race ./otlp/export/... ./internal/exportutil/... — endpoint derivation (Datadog vs collector, trailing-slash trim, schemeless/non-HTTP rejection), header injection (dd-api-key; dd-otel-metric-config metrics+Datadog-route-only, incl. the Endpoint+APIKey override case), protobuf round-trip, per-request result rows, OTLP retry classification (503/502 retry, 500 permanent) with exact attempt counts, partial-success rejection for all three signals, decoded-status snippet, doPost 2xx read-error + Retry-After header threading, and the Retry engine directly (RetryAfter override, ctx-cancel mid-wait, MaxAttempts, custom classifier). golangci-lint (incl. the modernize analyzer) is clean.

Review status

Reviewed clean by Codex on the current HEAD (no findings).

Open question (RFC §4)

Package placement (otlp/export) and whether to expose/generalize the existing ddtrace/tracer OTLP transport are @DataDog/apm-go design calls; this PR colocates a new generalized raw-proto transport under the package. Intake-contract confirm for the agentless OTLP Export*ServiceRequest wrappers is flagged for intake owners.

🤖 Generated with Claude Code

@khanayan123 khanayan123 requested a review from a team as a code owner July 7, 2026 19:10
@datadog-official

datadog-official Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 79.90%
Overall Coverage: 62.93% (+0.10%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d26661f | Docs | Datadog PR Page | Give us feedback!

@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: 8657c2d694

ℹ️ 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 otlp/export/transport.go Outdated
Comment on lines +141 to +142
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
return resp.StatusCode, respBody, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Surface OTLP partial-success drops

When a collector or Datadog-compatible endpoint returns HTTP 200 with an OTLP partial_success response that has rejected_spans, rejected_data_points, or rejected_log_records set, this branch reports the request as successful and ExportResult.OK() stays true even though data was dropped. Decode the signal-specific response proto on 2xx and return a non-retriable per-request error when the rejected count is non-zero so offline exporters do not silently lose telemetry.

Useful? React with 👍 / 👎.

Comment thread otlp/export/transport.go Outdated
Comment thread otlp/export/transport.go Outdated
@pr-commenter

pr-commenter Bot commented Jul 7, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-09 17:13:08

Comparing candidate commit d26661f in PR branch ayan.khan/otlp-export with baseline commit 46ab716 in branch ayan.khan/llmobs-explicit-span-ids.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 326 metrics, 0 unstable metrics, 1 flaky benchmarks without significant changes.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

Known flaky benchmarks without significant changes:

  • scenario:BenchmarkOTLPTraceWriterFlush

@khanayan123 khanayan123 marked this pull request as draft July 8, 2026 13:33
@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 8657c2d to 14a594f Compare July 9, 2026 04:18
@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 14a594f8c6

ℹ️ 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".

@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 14a594f to 195f2e6 Compare July 9, 2026 05:18
@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 195f2e66a3

ℹ️ 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".

@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 195f2e6 to 9111a00 Compare July 9, 2026 05:34
@khanayan123

Copy link
Copy Markdown
Contributor Author

@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: 9111a00ab0

ℹ️ 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 otlp/export/transport.go Outdated
Comment thread otlp/export/transport.go Outdated
Comment thread internal/exportutil/retry.go Outdated
@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 9111a00 to bbd3993 Compare July 9, 2026 06:01
@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: bbd39933b9

ℹ️ 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".

@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from bbd3993 to 41cdd9b Compare July 9, 2026 06:17
@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 41cdd9bb72

ℹ️ 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".

@khanayan123

Copy link
Copy Markdown
Contributor Author

@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: 41cdd9bb72

ℹ️ 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 internal/exportutil/retry.go
Comment thread otlp/export/transport.go Outdated
Comment thread otlp/export/transport.go Outdated
@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch 2 times, most recently from 33dd489 to 8a0f97b Compare July 9, 2026 15:42
@khanayan123

Copy link
Copy Markdown
Contributor Author

@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: 8a0f97b5a6

ℹ️ 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 otlp/export/transport.go Outdated
}
defer resp.Body.Close()

respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle response body read failures

When a collector or intake sends 2xx headers but the response body read fails, for example from a reset connection or mismatched Content-Length, this discards the read error and the following success branch reports the export as delivered. Because the body is what carries OTLP partial-success rejections, an unreadable response can hide dropped records; return an Attempt error when ReadAll fails so the request is surfaced as failed/retryable instead of successful.

Useful? React with 👍 / 👎.

Comment thread otlp/export/transport.go
if vn < 0 {
return ""
}
return string(v)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Keep decoded status snippets bounded

If an OTLP endpoint returns a google.rpc.Status protobuf with a large or non-UTF-8 message field, this bypasses exportutil.Snippet and stores the decoded string directly in RequestResult.ResponseSnippet. That field is documented as a bounded, UTF-8-safe excerpt, so callers that log it can unexpectedly emit up to the full 1 MiB response limit or invalid text; run the decoded message through the same snippet path before returning it.

Useful? React with 👍 / 👎.

@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 8a0f97b to 99af138 Compare July 9, 2026 16:12
@khanayan123

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 99af1385d4

ℹ️ 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".

khanayan123 and others added 3 commits July 9, 2026 12:42
Add a public otlp/export package: offline clients that POST already-built
OTLP collector proto requests (go.opentelemetry.io/proto/otlp/collector/
{trace,metrics,logs}/v1) as raw protobuf to Datadog's agentless OTLP intake
(otlp.<site>/v1/<signal>) or a caller-provided collector/Agent endpoint.

This is the OTLP half of the "Raw LLM Observability Export" RFC, stacked on
the llmobs/export PR. It uses a raw-proto transport and coexists with the
OTel-SDK-based exporters in ddtrace/opentelemetry (does not replace them).

- TraceClient/MetricClient/LogClient with ExportTraces/ExportMetrics/
  ExportLogs; each input request is atomic (one request -> one POST -> one
  indexed RequestResult); the SDK never merges or splits requests.
- Datadog route (Site+APIKey -> dd-api-key) or collector/Agent route
  (Endpoint, no auth). Metrics on the Datadog route add dd-otel-metric-config
  so exponential histograms render as distributions.
- Bounded retry (default 3 attempts; 5xx/408/429/network transient, other 4xx
  permanent, caller-cancelled context not retriable), per-request results.
- Caller trace IDs/flags/tracestate preserved as-is; callers own proto
  construction, projection, temporality, dedup, and durable retry.

Reuse (per the codebase reuse audit): the HTTP client comes from the shared
internal.DefaultHTTPClient, and retry/backoff/classification, response-body
snippets and failure aggregation live in internal/exportutil (shared with
llmobs/export) rather than being hand-rolled here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ages

Address the Codex review for the OTLP transport:

- Restrict retries to the OTLP/HTTP retryable set (429, 502, 503, 504, and
  network errors). A 500/501/505 or 408 is now permanent instead of burning
  every attempt. exportutil.Retry gains a pluggable Retriable predicate so the
  llmobs path keeps its own (broader) classification.
- Honor Retry-After on throttling responses: doPost parses the header
  (delta-seconds or HTTP-date, clamped to 60s) and Retry waits at least that
  long before the next attempt instead of its 100ms backoff.
- Surface decoded google.rpc.Status messages in ResponseSnippet for failed
  requests (extracted with protowire, no new dependency) instead of raw
  protobuf control bytes.

Adds internal unit tests for the classifier, Retry-After parsing, and status
decoding, plus behavioral tests for non-retryable 500 and the decoded snippet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s snippets

Address the Codex review plus an adversarial self-review of the diff:

- doPost now surfaces an io.ReadAll failure on a 2xx response as a transport-
  class (retryable) error instead of silently reporting success. The 2xx body
  carries OTLP partial-success rejections, so an unreadable body could otherwise
  hide dropped records.
- Decoded google.rpc.Status messages now go through exportutil.Snippet, so
  ResponseSnippet stays bounded and UTF-8-safe even for a large/non-UTF-8
  server message.
- internal/README: correct the exportutil description — only Snippet/Aggregate
  are shared by both clients; Retry/Retriable are used by otlp/export (llmobs
  retries via the internal LLM Obs transport).

Tests: add internal/exportutil retry_test.go (RetryAfter override, ctx-cancel
mid-wait, MaxAttempts, non-retriable stop, custom classifier); doPost tests for
the 2xx read-error and Retry-After header threading; and metric + log
partial-success rejection tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@khanayan123 khanayan123 force-pushed the ayan.khan/otlp-export branch from 99af138 to d26661f Compare July 9, 2026 16:43
@khanayan123

Copy link
Copy Markdown
Contributor Author

@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: d26661f73e

ℹ️ 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 otlp/export/transport.go
Comment on lines +232 to +233
return &http.Client{
Transport: &http.Transport{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Disable redirects for OTLP POSTs

When the configured endpoint or an intermediate proxy returns a 301/302/303 redirect, Go's default http.Client follows it by replaying the request as a GET with the body dropped, so a final 2xx page can be reported as a successful export even though the OTLP protobuf was never posted. The default redirect policy also forwards custom headers such as dd-api-key, so redirects can leak credentials to the redirected host. Please install a CheckRedirect policy for the default client, such as returning http.ErrUseLastResponse so 3xx responses are surfaced as failed exports instead of silent data loss.

Useful? React with 👍 / 👎.

Comment thread otlp/export/transport.go
Comment on lines +141 to +148
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
if readErr != nil {
// The 2xx body carries OTLP partial-success rejections; a failed read
// could hide dropped records, so surface it as a transport-class
// (retryable, status 0) failure instead of reporting full success.
return exportutil.Attempt{Body: respBody, Err: fmt.Errorf("otlp/export: read response body (HTTP %d): %w", resp.StatusCode, readErr)}
}
return exportutil.Attempt{Status: resp.StatusCode, Body: respBody}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require valid HTTP 200 OTLP success responses

OTLP/HTTP success and partial-success responses are HTTP 200 OK with a protobuf Export<Service>Response body, but this accepts every 2xx as delivered and later treats an unparsable body as zero rejections. If a proxy or misconfigured endpoint returns 202, 204, 206, or a 200 HTML/login page, Export* can return OK even though no OTLP response was received and partial rejections could not be inspected; please only report success after a 200 response whose body decodes as the expected OTLP response message.

Useful? React with 👍 / 👎.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant