Skip to content

Commit 042cf47

Browse files
authored
[OTEL] OTLP Logs API Support (#7680)
## Summary of changes This PR implements OTLP Logs export for ILogger as part of the OpenTelemetry Logs RFC Key additions: - Custom OTLP Serializer: Protobuf writer using vendored OpenTelemetry utilities (no SDK dependency) - Multi-Protocol Support: gRPC transports and HTTP/protobuf - Auto-Instrumentation: Zero-touch enablement via DD_LOGS_OTEL_ENABLED=true - Configurations: Full support for OTEL_EXPORTER_OTLP_* environment variables ## Reason for change The [RFC] OpenTelemetry Logs API support enables customers using OpenTelemetry standards to migrate to Datadog SDK with minimal friction while allowing customers to continue to receive their logs as expected. ## Implementation details ✅ Core Components: - `OtlpLogsSerializer` - Custom protobuf writer using vendored OTLP field constants (no external dependencies) - `OtlpExporter` - Multi-protocol exporter (HTTP/protobuf and gRPC) with configurable endpoint/headers/timeout - `OtlpSubmissionLogSink` - Batching sink that integrates with existing Direct Log Submission pipeline - `LogPoint` - Internal data model representing OTLP log records with trace/span IDs and attributes ✅ Vendored OpenTelemetry Components: - `ProtobufSerializer` - Low-level protobuf writer - `ProtobufOtlpLogFieldNumberConstants` - Official OTLP field numbers - `OtlpHttpExportClient` - HTTP/protobuf transport client ✅ Integration: - `DirectSubmissionLogger` auto-instrumentation detects `OtlpSubmissionLogSink`and creates `LogPoint` objects - When OTLP logs are enabled, they take precedence over Datadog Direct Log Submission to send logs over OTLP instead. ## Test coverage Unit Tests: - OtlpSinkTests - Validates batching, trace context injection, attribute handling, severity mapping - TracerSettingsTests - Configuration parsing (protocol, endpoint, headers, timeout fallbacks) Integration Tests: - OpenTelemetrySdkTests.SubmitsOtlpLogs - End-to-end validation with snapshot testing - Verifies OTLP payload structure and resource attributes - Confirms trace-log correlation with active spans
1 parent 68e8ee4 commit 042cf47

File tree

41 files changed

+2666
-125
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2666
-125
lines changed

tracer/build/_build/UpdateVendors/VendoredDependency.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,11 +167,20 @@ static VendoredDependency()
167167
AddOpenTelemetryUsings),
168168
relativePathsToExclude: new[]
169169
{
170-
// Vendor only the gRPC transport client - exclude everything else
170+
// Vendor gRPC and HTTP export clients for logs and metrics
171+
// Vendor low-level protobuf utilities: ProtobufSerializer, ProtobufWireType
172+
// Vendor ONLY field constants we actually use (Logs and Common)
173+
// EXCLUDE high-level serializers that depend on OpenTelemetry SDK types
171174
".publicApi/",
172175
"Builder/",
173176
"PersistentStorage/",
174-
"Implementation/Serializer/",
177+
"Implementation/Serializer/ProtobufOtlpLogSerializer.cs",
178+
"Implementation/Serializer/ProtobufOtlpMetricSerializer.cs",
179+
"Implementation/Serializer/ProtobufOtlpTraceSerializer.cs",
180+
"Implementation/Serializer/ProtobufOtlpResourceSerializer.cs",
181+
"Implementation/Serializer/ProtobufOtlpTagWriter.cs",
182+
"Implementation/Serializer/ProtobufOtlpMetricFieldNumberConstants.cs", // Not used - metrics has own FieldNumbers ATM
183+
"Implementation/Serializer/ProtobufOtlpTraceFieldNumberConstants.cs",
175184
"Implementation/Transmission/",
176185
"Implementation/ActivityExtensions.cs",
177186
"Implementation/ExperimentalOptions.cs",
@@ -182,7 +191,6 @@ static VendoredDependency()
182191
"Implementation/OtlpExporterOptionsConfigurationType.cs",
183192
"Implementation/OtlpSpecConfigDefinitions.cs",
184193
"Implementation/TimestampHelpers.cs",
185-
"Implementation/ExportClient/OtlpHttpExportClient.cs", // We only need gRPC for this PR
186194
"Implementation/ExportClient/OtlpRetry.cs",
187195
"CHANGELOG.md",
188196
"README.md",

tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@
237237
<type fullname="System.Diagnostics.DebuggerTypeProxyAttribute" />
238238
</assembly>
239239
<assembly fullname="System.Diagnostics.DiagnosticSource">
240+
<type fullname="System.Diagnostics.Activity" />
241+
<type fullname="System.Diagnostics.ActivityIdFormat" />
242+
<type fullname="System.Diagnostics.ActivitySpanId" />
243+
<type fullname="System.Diagnostics.ActivityTraceId" />
240244
<type fullname="System.Diagnostics.DiagnosticListener" />
241245
<type fullname="System.Diagnostics.DistributedContextPropagator" />
242246
<type fullname="System.Diagnostics.Metrics.Instrument" />
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// <copyright file="DatadogLogEventCreator.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
#nullable enable
6+
7+
using System;
8+
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSubmission.Formatting;
9+
using Datadog.Trace.Logging.DirectSubmission.Formatting;
10+
11+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSubmission;
12+
13+
/// <summary>
14+
/// Creates log events in Datadog format
15+
/// </summary>
16+
internal class DatadogLogEventCreator : ILogEventCreator
17+
{
18+
private readonly LogFormatter _logFormatter;
19+
private readonly IExternalScopeProvider? _scopeProvider;
20+
21+
public DatadogLogEventCreator(LogFormatter logFormatter, IExternalScopeProvider? scopeProvider)
22+
{
23+
_logFormatter = logFormatter;
24+
_scopeProvider = scopeProvider;
25+
}
26+
27+
public LoggerDirectSubmissionLogEvent CreateLogEvent<TState>(
28+
int logLevel,
29+
string categoryName,
30+
object eventId,
31+
TState state,
32+
Exception? exception,
33+
Func<TState, Exception?, string> formatter)
34+
{
35+
var logEntry = new LogEntry<TState>(
36+
DateTime.UtcNow,
37+
logLevel,
38+
categoryName,
39+
eventId.GetHashCode(),
40+
state,
41+
exception,
42+
formatter,
43+
_scopeProvider);
44+
45+
var serializedLog = LoggerLogFormatter.FormatLogEvent(_logFormatter, logEntry);
46+
return new LoggerDirectSubmissionLogEvent(serializedLog);
47+
}
48+
49+
public IDisposable BeginScope<TState>(TState state)
50+
{
51+
return _scopeProvider?.Push(state) ?? NullDisposable.Instance;
52+
}
53+
54+
private class NullDisposable : IDisposable
55+
{
56+
public static readonly NullDisposable Instance = new();
57+
58+
public void Dispose()
59+
{
60+
}
61+
}
62+
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Logging/ILogger/DirectSubmission/DirectSubmissionLogger.cs

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
#nullable enable
66

77
using System;
8-
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSubmission.Formatting;
98
using Datadog.Trace.DuckTyping;
109
using Datadog.Trace.Logging.DirectSubmission;
11-
using Datadog.Trace.Logging.DirectSubmission.Formatting;
1210
using Datadog.Trace.Logging.DirectSubmission.Sink;
1311
using Datadog.Trace.Telemetry;
1412
using Datadog.Trace.Telemetry.Metrics;
@@ -21,22 +19,19 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSu
2119
internal class DirectSubmissionLogger
2220
{
2321
private readonly string _name;
24-
private readonly IExternalScopeProvider? _scopeProvider;
2522
private readonly IDirectSubmissionLogSink _sink;
26-
private readonly LogFormatter? _logFormatter;
23+
private readonly ILogEventCreator _logEventCreator;
2724
private readonly int _minimumLogLevel;
2825

2926
internal DirectSubmissionLogger(
3027
string name,
31-
IExternalScopeProvider? scopeProvider,
3228
IDirectSubmissionLogSink sink,
33-
LogFormatter? logFormatter,
29+
ILogEventCreator logEventCreator,
3430
DirectSubmissionLogLevel minimumLogLevel)
3531
{
3632
_name = name;
37-
_scopeProvider = scopeProvider;
3833
_sink = sink;
39-
_logFormatter = logFormatter;
34+
_logEventCreator = logEventCreator;
4035
_minimumLogLevel = (int)minimumLogLevel;
4136
}
4237

@@ -57,24 +52,7 @@ public void Log<TState>(int logLevel, object eventId, TState state, Exception? e
5752
return;
5853
}
5954

60-
// We render the event to a string immediately as we need to capture the properties
61-
// This is more expensive from a CPU perspective, but saves having to persist the
62-
// properties to a dictionary and rendering later
63-
64-
var logEntry = new LogEntry<TState>(
65-
DateTime.UtcNow,
66-
logLevel,
67-
_name,
68-
eventId.GetHashCode(),
69-
state,
70-
exception,
71-
formatter,
72-
_scopeProvider);
73-
var logFormatter = _logFormatter ?? TracerManager.Instance.DirectLogSubmission.Formatter;
74-
var serializedLog = LoggerLogFormatter.FormatLogEvent(logFormatter, logEntry);
75-
76-
var log = new LoggerDirectSubmissionLogEvent(serializedLog);
77-
55+
var log = _logEventCreator.CreateLogEvent(logLevel, _name, eventId, state, exception, formatter);
7856
TelemetryFactory.Metrics.RecordCountDirectLogLogs(MetricTags.IntegrationName.ILogger);
7957
_sink.EnqueueLog(log);
8058
}
@@ -94,15 +72,6 @@ public void Log<TState>(int logLevel, object eventId, TState state, Exception? e
9472
/// <typeparam name="TState">The type of the state to begin scope for.</typeparam>
9573
/// <returns>An <see cref="IDisposable"/> that ends the logical operation scope on dispose.</returns>
9674
[DuckReverseMethod(ParameterTypeNames = new[] { "TState" })]
97-
public IDisposable BeginScope<TState>(TState state) => _scopeProvider?.Push(state) ?? NullDisposable.Instance;
98-
99-
private class NullDisposable : IDisposable
100-
{
101-
public static readonly NullDisposable Instance = new();
102-
103-
public void Dispose()
104-
{
105-
}
106-
}
75+
public IDisposable BeginScope<TState>(TState state) => _logEventCreator.BeginScope(state);
10776
}
10877
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Logging/ILogger/DirectSubmission/DirectSubmissionLoggerProvider.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
using System;
88
using System.Collections.Concurrent;
9-
using System.ComponentModel;
109
using Datadog.Trace.DuckTyping;
1110
using Datadog.Trace.Logging.DirectSubmission;
1211
using Datadog.Trace.Logging.DirectSubmission.Formatting;
@@ -23,9 +22,9 @@ internal class DirectSubmissionLoggerProvider
2322
private readonly Func<string, DirectSubmissionLogger> _createLoggerFunc;
2423
private readonly ConcurrentDictionary<string, DirectSubmissionLogger> _loggers = new();
2524
private readonly IDirectSubmissionLogSink _sink;
26-
private readonly LogFormatter? _formatter;
2725
private readonly DirectSubmissionLogLevel _minimumLogLevel;
28-
private IExternalScopeProvider? _scopeProvider;
26+
private readonly LogFormatter? _logFormatter;
27+
private ILogEventCreator _logEventCreator;
2928

3029
internal DirectSubmissionLoggerProvider(IDirectSubmissionLogSink sink, DirectSubmissionLogLevel minimumLogLevel, IExternalScopeProvider? scopeProvider)
3130
: this(sink, formatter: null, minimumLogLevel, scopeProvider)
@@ -40,10 +39,19 @@ internal DirectSubmissionLoggerProvider(
4039
IExternalScopeProvider? scopeProvider)
4140
{
4241
_sink = sink;
43-
_formatter = formatter;
4442
_minimumLogLevel = minimumLogLevel;
4543
_createLoggerFunc = CreateLoggerImplementation;
46-
_scopeProvider = scopeProvider;
44+
45+
#if NETCOREAPP3_1_OR_GREATER
46+
if (sink is OtlpSubmissionLogSink)
47+
{
48+
_logEventCreator = new OtelLogEventCreator();
49+
return;
50+
}
51+
#endif
52+
53+
_logFormatter = formatter ?? TracerManager.Instance.DirectLogSubmission.Formatter;
54+
_logEventCreator = new DatadogLogEventCreator(_logFormatter, scopeProvider);
4755
}
4856

4957
/// <summary>
@@ -59,7 +67,7 @@ public DirectSubmissionLogger CreateLogger(string categoryName)
5967

6068
private DirectSubmissionLogger CreateLoggerImplementation(string name)
6169
{
62-
return new DirectSubmissionLogger(name, _scopeProvider, _sink, _formatter, _minimumLogLevel);
70+
return new DirectSubmissionLogger(name, _sink, _logEventCreator, _minimumLogLevel);
6371
}
6472

6573
/// <inheritdoc cref="IDisposable.Dispose"/>
@@ -75,7 +83,10 @@ public void Dispose()
7583
[DuckReverseMethod(ParameterTypeNames = new[] { "Microsoft.Extensions.Logging.IExternalScopeProvider, Microsoft.Extensions.Logging.Abstractions" })]
7684
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
7785
{
78-
_scopeProvider = scopeProvider;
86+
if (_logFormatter != null)
87+
{
88+
_logEventCreator = new DatadogLogEventCreator(_logFormatter, scopeProvider);
89+
}
7990
}
8091
}
8192
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// <copyright file="OtlpLogEventBuilder.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#if NETCOREAPP3_1_OR_GREATER
7+
#nullable enable
8+
9+
using System;
10+
using System.Collections.Generic;
11+
using Datadog.Trace.OpenTelemetry.Logs;
12+
using Datadog.Trace.Util;
13+
14+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSubmission.Formatting
15+
{
16+
/// <summary>
17+
/// Helper class to build OTLP log events from ILogger log calls.
18+
/// Extracts structured data and trace context for OTLP export.
19+
/// </summary>
20+
internal static class OtlpLogEventBuilder
21+
{
22+
/// <summary>
23+
/// Creates a LoggerDirectSubmissionLogEvent with OTLP structured data from ILogger state.
24+
/// </summary>
25+
public static LoggerDirectSubmissionLogEvent CreateLogEvent<TState>(
26+
int logLevel,
27+
string categoryName,
28+
object eventId,
29+
TState state,
30+
Exception? exception,
31+
Func<TState, Exception?, string> formatter)
32+
{
33+
var message = formatter(state, exception);
34+
var attributes = ExtractAttributes(state, eventId);
35+
var (traceId, spanId, flags) = ExtractTraceContext();
36+
37+
return new LoggerDirectSubmissionLogEvent(null)
38+
{
39+
OtlpLog = new LogPoint
40+
{
41+
Message = message,
42+
LogLevel = logLevel,
43+
CategoryName = categoryName,
44+
Timestamp = DateTimeOffset.UtcNow.DateTime,
45+
Exception = exception,
46+
Attributes = attributes,
47+
TraceId = traceId,
48+
SpanId = spanId,
49+
Flags = flags
50+
}
51+
};
52+
}
53+
54+
private static Dictionary<string, object?> ExtractAttributes<TState>(TState state, object eventId)
55+
{
56+
var attributes = new Dictionary<string, object?>();
57+
58+
// Extract structured properties from ILogger state
59+
if (state is IReadOnlyList<KeyValuePair<string, object?>> properties)
60+
{
61+
foreach (var property in properties)
62+
{
63+
if (!string.IsNullOrEmpty(property.Key) && property.Key != "{OriginalFormat}")
64+
{
65+
attributes[property.Key] = property.Value;
66+
}
67+
}
68+
}
69+
70+
// Add EventId if present
71+
if (eventId.GetHashCode() != 0)
72+
{
73+
attributes["EventId"] = eventId.GetHashCode();
74+
}
75+
76+
return attributes;
77+
}
78+
79+
private static (TraceId TraceId, ulong SpanId, int Flags) ExtractTraceContext()
80+
{
81+
// Prefer Datadog's trace context for accurate correlation with DD spans
82+
// Fallback to Activity if Datadog tracing is not active
83+
var ddSpan = Tracer.Instance.ActiveScope?.Span as Span;
84+
if (ddSpan != null)
85+
{
86+
return (ddSpan.TraceId128, ddSpan.SpanId, SamplingPriorityValues.IsKeep(ddSpan.Context.SamplingPriority!.Value) ? 1 : 0);
87+
}
88+
89+
var activity = System.Diagnostics.Activity.Current;
90+
if (activity != null && activity.IdFormat == System.Diagnostics.ActivityIdFormat.W3C)
91+
{
92+
if (HexString.TryParseTraceId(activity.TraceId.ToHexString(), out var activityTraceId) &&
93+
HexString.TryParseUInt64(activity.SpanId.ToHexString(), out var activitySpanId))
94+
{
95+
var flags = activity.Recorded ? 1 : 0;
96+
return (activityTraceId, activitySpanId, flags);
97+
}
98+
}
99+
100+
return (TraceId.Zero, 0, 0);
101+
}
102+
}
103+
}
104+
105+
#endif
106+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// <copyright file="ILogEventCreator.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
#nullable enable
6+
7+
using System;
8+
9+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Logging.ILogger.DirectSubmission;
10+
11+
/// <summary>
12+
/// Creates DirectSubmissionLogEvent instances from ILogger calls
13+
/// </summary>
14+
internal interface ILogEventCreator
15+
{
16+
LoggerDirectSubmissionLogEvent CreateLogEvent<TState>(
17+
int logLevel,
18+
string categoryName,
19+
object eventId,
20+
TState state,
21+
Exception? exception,
22+
Func<TState, Exception?, string> formatter);
23+
24+
IDisposable BeginScope<TState>(TState state);
25+
}

0 commit comments

Comments
 (0)