Skip to content

Commit 35f65d0

Browse files
[Tracing] Add experimental http/protobuf support for OTLP traces export (#8645)
## Summary of changes Adds `http/protobuf` support to the OTLP traces exporter introduced in #8211, which introduced the exporter and `http/json` support. This feature is enabled by setting `OTEL_TRACES_EXPORTER=otlp` and `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf` (or the parent `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`). ## Reason for change When #8211 landed, `http/json` was the only functional OTLP traces protocol — setting any other protocol would fall back (with a log message) to the Datadog MessagePack encoding. `http/protobuf` is the OTLP default protocol per the OpenTelemetry specification and is what most OTel collectors, Datadog Agent included, expect to receive, so we need to support it to be useful out-of-the-box for users adopting the OTLP traces export. ## Implementation details ### Serializer Adds `Datadog.Trace.OpenTelemetry.Traces.OtlpTracesProtobufSerializer`, a new `ISpanBufferSerializer` that emits the [OTLP `ExportTraceServiceRequest`](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.2.0/opentelemetry/proto/trace/v1/trace.proto) protobuf payload directly into `SpanBuffer`'s output buffer: - The `resource_spans` and `scope_spans` envelopes are opened on the first `SerializeSpans` call, span entries are appended per chunk, and `FinishBody` patches the two length-prefix placeholders using absolute positions into the destination buffer. - The serializer respects the same per-span / per-event / per-link attribute count limits (`128`) as `OtlpTracesJsonSerializer`, and re-uses the `OtlpMapper` logic to map resource attributes and tags onto the serialized spans. - `OtlpTracesProtobufSerializer` is **stateful** (it tracks the two length-placeholder positions), so `AgentWriter` now creates a fresh instance for the front and back buffers rather than sharing one — the previous shared `OtlpTracesJsonSerializer` was stateless and didn't need this. ### Vendored OpenTelemetry SDK Builds on the protobuf primitives vendored from the OpenTelemetry .NET SDK in #7653 (`ProtobufSerializer`, `ProtobufWireType`): - Adds new field-number constant files: `ProtobufOtlpTraceFieldNumberConstants` and `ProtobufOtlpResourceFieldNumberConstants` (split out so the OTLP trace serializer only depends on what it needs). - Removes the `#if NETCOREAPP3_1_OR_GREATER` guard from the vendored `ProtobufSerializer` — now that we vendor a `Span<>` polyfill the serializer compiles and runs on all supported runtimes (.NET Framework 4.6.1+). - Drops the matching `#if NET6_0_OR_GREATER` guard around the `SubmitsOtlpTraces` integration test (`http/protobuf` and `http/json` both run on .NET Framework now too). ### Wiring - `ExporterSettings`: `OtlpProtocol.HttpProtobuf` now resolves to `TracesEncoding.OtlpProtobuf` instead of falling back to `DatadogV0_4`. - `AgentWriter.GetSpanSerializer`: selects `OtlpTracesProtobufSerializer` when `TracesEncoding == OtlpProtobuf`. - `ApiOtlp.SendTracesAsync`: chooses `application/x-protobuf` vs `application/json` for the request `Content-Type` based on `_tracesEncoding`. Adds the `application/x-protobuf` MIME type constant to `Datadog.Trace.Agent.Transports.MimeTypes`. - `supported-configurations.yaml`: documents `http/protobuf` as a valid value for `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` and regenerates `ConfigurationKeys.OpenTelemetry.g.cs` for all TFMs. ## Test coverage - **Unit tests**: New `OtlpTracesProtobufSerializerTests` exercises the serializer end-to-end using `Google.Protobuf` and a little custom parsing. Test cases include: - Empty buffer (`FinishBody` returns 0) - Multiple traces reported in a single-chunk - Serializing span events/links/status - Exceeding buffer `maxSize` honoring behavior - Exceeding span limits - **Integration tests**: `OpenTelemetrySdkTests.SubmitsOtlpTraces` now runs both `http/json` and `http/protobuf` cases for the Datadog SDK on all TFMs. - **Snapshot unification**: To keep one Datadog-SDK snapshot regardless of OTLP protocol, snapshots for `http/protobuf` are normalized to the `http/json` shape via scrubbers that translate the test agent's protobuf rendering (snake_case field names, string-form enum values like `SPAN_KIND_SERVER`) into the JSON serializer's shape (camelCase field names, integer enum values). - **Snapshot rename**: `SubmitsOtlpTraces_DD_http_json.verified.txt` renamed to `SubmitsOtlpTraces_DD.verified.txt` (one snapshot now covers both protocols). ## Other details With this PR, `http/json` and `http/protobuf` are both functional; `grpc` still falls back to Datadog MessagePack encoding with a startup warning. ### Follow-up work - Implement the `grpc` OTLP traces protocol - Add OTLP traces telemetry counts (the `TODO: Telemetry - Record OTLP Traces API submissions` in `ApiOtlp.SendTracesAsync` still applies) - Honor `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` in `ApiOtlp` (still unused) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dc3f873 commit 35f65d0

35 files changed

Lines changed: 7959 additions & 149 deletions

File tree

tracer/src/Datadog.Trace/Agent/AgentWriter.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,18 @@ internal AgentWriter(IApi api, IStatsAggregator? statsAggregator, IStatsdManager
9494
_batchInterval = batchInterval;
9595
_traceKeepRateCalculator = traceKeepRateCalculator;
9696

97-
ISpanBufferSerializer spanBufferSerializer = api.TracesEncoding switch
97+
ISpanBufferSerializer CreateSpanSerializer() => api.TracesEncoding switch
9898
{
9999
TracesEncoding.OtlpJson => new OtlpTracesJsonSerializer(),
100+
TracesEncoding.OtlpProtobuf => new OtlpTracesProtobufSerializer(),
100101
_ => new SpanBufferMessagePackSerializer(SpanFormatterResolver.Instance),
101102
};
102103

103104
_forceFlush = new TaskCompletionSource<bool>(TaskOptions);
104105

105-
_frontBuffer = new SpanBuffer(maxBufferSize, spanBufferSerializer);
106-
_backBuffer = new SpanBuffer(maxBufferSize, spanBufferSerializer);
106+
// OtlpTracesProtobufSerializer is stateful, so we need to create a new instance for each buffer.
107+
_frontBuffer = new SpanBuffer(maxBufferSize, CreateSpanSerializer());
108+
_backBuffer = new SpanBuffer(maxBufferSize, CreateSpanSerializer());
107109
_activeBuffer = _frontBuffer;
108110

109111
_apmTracingEnabled = apmTracingEnabled;

tracer/src/Datadog.Trace/Agent/ApiOtlp.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6-
#if NET6_0_OR_GREATER
7-
86
using System;
97
using System.Collections.Generic;
108
using System.IO;
@@ -37,7 +35,6 @@ internal sealed class ApiOtlp : IApi
3735
private readonly Uri _statsEndpoint; // This endpoint is passed for the _sendStats callback, but otherwise unused
3836
private readonly SendCallback<SendStatsState> _sendStats;
3937
private readonly SendCallback<SendTracesState> _sendTraces;
40-
private readonly Datadog.Trace.OpenTelemetry.Metrics.OtlpExporter _metricsExporter;
4138

4239
public ApiOtlp(IApiRequestFactory apiRequestFactory, TracerSettings settings, ExporterSettings exporterSettings, IDatadogLogger log = null)
4340
{
@@ -53,8 +50,6 @@ public ApiOtlp(IApiRequestFactory apiRequestFactory, TracerSettings settings, Ex
5350
_tracesHeaders = exporterSettings.OtlpTracesHeaders ?? [];
5451
_statsEndpoint = exporterSettings.OtlpMetricsEndpoint;
5552
_log.Debug("Using traces endpoint {TracesEndpoint}", _tracesEndpoint.ToString());
56-
57-
_metricsExporter = new Datadog.Trace.OpenTelemetry.Metrics.OtlpExporter(settings, exporterSettings);
5853
}
5954

6055
private delegate Task<SendResult> SendCallback<T>(IApiRequest request, bool isFinalTry, T state);
@@ -196,8 +191,13 @@ private async Task<SendResult> SendTracesAsyncImpl(IApiRequest request, bool fin
196191
try
197192
{
198193
// TODO: Telemetry - Record OTLP Traces API submissions
199-
// TODO: Add more precise logic for "application/x-protobuf" vs "application/json"
200-
response = await request.PostAsync(traces, MimeTypes.Json).ConfigureAwait(false);
194+
var contentType = _tracesEncoding switch
195+
{
196+
TracesEncoding.OtlpProtobuf => MimeTypes.XProtobuf,
197+
_ => MimeTypes.Json,
198+
};
199+
200+
response = await request.PostAsync(traces, contentType).ConfigureAwait(false);
201201
}
202202
catch (Exception)
203203
{
@@ -304,4 +304,3 @@ public SendStatsState(StatsBuffer stats, long bucketDuration, int tracerObfuscat
304304
}
305305
}
306306
}
307-
#endif

tracer/src/Datadog.Trace/Agent/ManagedApiOtlp.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
44
// </copyright>
55

6-
#if NET6_0_OR_GREATER
7-
86
#nullable enable
97

108
using System;
@@ -50,5 +48,3 @@ public Task<bool> SendTracesAsync(ArraySegment<byte> traces, int numberOfTraces,
5048
public Task<bool> SendStatsAsync(StatsBuffer stats, long bucketDuration, int tracerObfuscationVersion)
5149
=> Volatile.Read(ref _api).SendStatsAsync(stats, bucketDuration, tracerObfuscationVersion);
5250
}
53-
54-
#endif

tracer/src/Datadog.Trace/Agent/Transports/MimeTypes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ internal static class MimeTypes
99
{
1010
public const string MsgPack = "application/msgpack";
1111
public const string Json = "application/json";
12+
public const string XProtobuf = "application/x-protobuf";
1213
public const string MultipartFormData = "multipart/form-data";
1314
public const string PlainText = "text/plain";
1415
public const string Gzip = "application/gzip";

tracer/src/Datadog.Trace/Configuration/ExporterSettings.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ internal ExporterSettings(Raw rawSettings, Func<string, bool> fileExists, IConfi
112112
{
113113
TracesEncoding = otlpTraceSettings.OtlpProtocol switch
114114
{
115-
OtlpProtocol.Grpc => TracesEncoding.DatadogV0_4,
116-
OtlpProtocol.HttpProtobuf => TracesEncoding.DatadogV0_4,
115+
OtlpProtocol.HttpProtobuf => TracesEncoding.OtlpProtobuf,
117116
OtlpProtocol.HttpJson => TracesEncoding.OtlpJson,
118117
_ => TracesEncoding.DatadogV0_4,
119118
};

tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2676,7 +2676,7 @@ supportedConfigurations:
26762676
documentation: |-
26772677
Configuration key to set the OTLP protocol for traces export.
26782678
Takes precedence over <see cref="ExporterOtlpProtocol"/>.
2679-
Valid values: http/json. Default: http/json
2679+
Valid values: http/json, http/protobuf. Default: http/json
26802680
OTEL_EXPORTER_OTLP_TRACES_TIMEOUT:
26812681
- implementation: B
26822682
type: int

tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.OpenTelemetry.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ internal static class OpenTelemetry
136136
/// <summary>
137137
/// Configuration key to set the OTLP protocol for traces export.
138138
/// Takes precedence over <see cref="ExporterOtlpProtocol"/>.
139-
/// Valid values: http/json. Default: http/json
139+
/// Valid values: http/json, http/protobuf. Default: http/json
140140
/// </summary>
141141
public const string ExporterOtlpTracesProtocol = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
142142

tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.OpenTelemetry.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ internal static class OpenTelemetry
136136
/// <summary>
137137
/// Configuration key to set the OTLP protocol for traces export.
138138
/// Takes precedence over <see cref="ExporterOtlpProtocol"/>.
139-
/// Valid values: http/json. Default: http/json
139+
/// Valid values: http/json, http/protobuf. Default: http/json
140140
/// </summary>
141141
public const string ExporterOtlpTracesProtocol = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
142142

tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.OpenTelemetry.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ internal static class OpenTelemetry
136136
/// <summary>
137137
/// Configuration key to set the OTLP protocol for traces export.
138138
/// Takes precedence over <see cref="ExporterOtlpProtocol"/>.
139-
/// Valid values: http/json. Default: http/json
139+
/// Valid values: http/json, http/protobuf. Default: http/json
140140
/// </summary>
141141
public const string ExporterOtlpTracesProtocol = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
142142

tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.OpenTelemetry.g.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ internal static class OpenTelemetry
136136
/// <summary>
137137
/// Configuration key to set the OTLP protocol for traces export.
138138
/// Takes precedence over <see cref="ExporterOtlpProtocol"/>.
139-
/// Valid values: http/json. Default: http/json
139+
/// Valid values: http/json, http/protobuf. Default: http/json
140140
/// </summary>
141141
public const string ExporterOtlpTracesProtocol = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
142142

0 commit comments

Comments
 (0)