diff --git a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt index e4f616a9dca..9d8f0a5adfc 100644 --- a/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/Experimental/PublicAPI.Unshipped.txt @@ -29,6 +29,8 @@ [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData() -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(in System.Diagnostics.ActivityContext activityContext) -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.LogRecordData(System.Diagnostics.Activity? activity) -> void +[OTEL1001]OpenTelemetry.Logs.LogRecordData.ObservedTimestamp.get -> System.DateTime +[OTEL1001]OpenTelemetry.Logs.LogRecordData.ObservedTimestamp.set -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.Severity.get -> OpenTelemetry.Logs.LogRecordSeverity? [OTEL1001]OpenTelemetry.Logs.LogRecordData.Severity.set -> void [OTEL1001]OpenTelemetry.Logs.LogRecordData.SeverityText.get -> string? diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index eca7ac930f5..8ed7cb8ea7c 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -6,6 +6,10 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added `ObservedTimestamp` property to `LogRecordData`. Note that `LogRecordData` + is only public in pre-release versions of the package. + ([#6979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6979)) + ## 1.15.1 Released 2026-Mar-27 diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs index 0889ce0b6fb..6c0fc63fb8a 100644 --- a/src/OpenTelemetry.Api/Logs/LogRecordData.cs +++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs @@ -28,13 +28,15 @@ struct LogRecordData { internal DateTime TimestampBacking = DateTime.UtcNow; + internal DateTime ObservedTimestampBacking = DateTime.UtcNow; + /// /// Initializes a new instance of the struct. /// /// /// Notes: /// - /// The property is initialized to The and properties are initialized to automatically. /// The , , and properties will be set using the struct. /// /// - /// Note: The property is initialized to and properties are initialized to automatically. /// /// Optional used to populate @@ -65,7 +67,7 @@ public LogRecordData(Activity? activity) /// Initializes a new instance of the struct. /// /// - /// Note: The property is initialized to and properties are initialized to automatically. /// /// used to @@ -92,6 +94,20 @@ public DateTime Timestamp set => this.TimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } + /// + /// Gets or sets the timestamp when the log was recorded by OpenTelemetry's code. + /// + /// + /// Note: If is set to a value with it will be automatically converted to + /// UTC using . + /// + public DateTime ObservedTimestamp + { + readonly get => this.ObservedTimestampBacking; + set { this.ObservedTimestampBacking = value.Kind == DateTimeKind.Local ? value.ToUniversalTime() : value; } + } + /// /// Gets or sets the log . /// diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index cdcb18d91a7..89ca0d22bb5 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* `ObservedTimestamp` will now be exported for logs. + ([#6979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6979)) + ## 1.15.1 Released 2026-Mar-27 diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs index f141c1268a6..80bde35a32b 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs @@ -48,6 +48,7 @@ public override ExportResult Export(in Batch batch) foreach (var logRecord in batch) { this.WriteLine($"{"LogRecord.Timestamp:",-RightPaddingLength}{logRecord.Timestamp:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); + this.WriteLine($"{"LogRecord.ObservedTimestamp:",-RightPaddingLength}{logRecord.ObservedTimestamp:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); if (logRecord.TraceId != default) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 569844dca56..78f6f58b86e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -7,6 +7,11 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* `observed_time_unix_nano` will no longer always be identical to `time_unix_nano` + when using the logs bridge API. By default, it will instead be set to the actual + observed time of the log record. + ([#6979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6979)) + ## 1.15.1 Released 2026-Mar-27 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs index 20f8c83cd54..17d955bbc99 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs @@ -180,8 +180,10 @@ internal static int WriteLogRecord(byte[] buffer, int writePosition, SdkLimitOpt otlpTagWriterState.WritePosition += ReserveSizeForLength; var timestamp = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(); + var observedTimestamp = logRecord.ObservedTimestamp == logRecord.Timestamp ? timestamp + : (ulong)logRecord.ObservedTimestamp.ToUnixTimeNanoseconds(); otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Time_Unix_Nano, timestamp); - otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Observed_Time_Unix_Nano, timestamp); + otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Observed_Time_Unix_Nano, observedTimestamp); otlpTagWriterState.WritePosition = ProtobufSerializer.WriteEnumWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Number, logRecord.Severity.HasValue ? (int)logRecord.Severity : 0); diff --git a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..bba16dff594 100644 --- a/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Logs.LogRecord.ObservedTimestamp.get -> System.DateTime +OpenTelemetry.Logs.LogRecord.ObservedTimestamp.set -> void diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index d16a820b081..875d7714da2 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -6,6 +6,9 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added `ObservedTimestamp` property to `LogRecord`. + ([#6979](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6979)) + ## 1.15.1 Released 2026-Mar-27 diff --git a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs index 69f07bba3e3..0b4b36c14d2 100644 --- a/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/ILogger/OpenTelemetryLogger.cs @@ -71,6 +71,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except ref var data = ref record.Data; data.TimestampBacking = DateTime.UtcNow; + data.ObservedTimestampBacking = data.TimestampBacking; SetLogRecordSeverityFields(ref data, logLevel); diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs index ddd196d3216..f86c193de24 100644 --- a/src/OpenTelemetry/Logs/LogRecord.cs +++ b/src/OpenTelemetry/Logs/LogRecord.cs @@ -51,6 +51,7 @@ internal LogRecord( this.Data = new(activity) { TimestampBacking = timestamp, + ObservedTimestampBacking = timestamp, Body = formattedMessage, }; @@ -114,6 +115,20 @@ public DateTime Timestamp set => this.Data.Timestamp = value; } + /// + /// Gets or sets the observed timestamp. + /// + /// + /// Note: If is set to a value with it will be automatically converted to + /// UTC using . + /// + public DateTime ObservedTimestamp + { + get => this.Data.ObservedTimestamp; + set => this.Data.ObservedTimestamp = value; + } + /// /// Gets or sets the log . /// diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs index 03d65ab3dd0..44b72656cff 100644 --- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs +++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs @@ -102,6 +102,25 @@ record = default; Assert.Equal(now.ToUniversalTime(), record.Timestamp); } + [Fact] + public void ObservedTimestampTest() + { + var nowUtc = DateTime.UtcNow; + + var record = new LogRecordData(); + Assert.True(record.ObservedTimestamp >= nowUtc); + + record = default; + Assert.Equal(DateTime.MinValue, record.ObservedTimestamp); + + var now = DateTime.Now; + + record.ObservedTimestamp = now; + + Assert.Equal(DateTimeKind.Utc, record.ObservedTimestamp.Kind); + Assert.Equal(now.ToUniversalTime(), record.ObservedTimestamp); + } + [Fact] public void SetActivityContextTest() { diff --git a/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs b/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs index 462c8e6dbc7..1c0f0f8c791 100644 --- a/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs +++ b/test/OpenTelemetry.Tests/Logs/LogRecordTests.cs @@ -1085,6 +1085,29 @@ public void CheckOriginalFormatAtArbitraryPosition(bool includeFormattedMessage, originalFormatAttribute.Value); } + [Fact] + public void ObservedTimestampTest() + { + using var loggerFactory = InitializeLoggerFactory(out var exportedItems); + var logger = loggerFactory.CreateLogger(); + + var before = DateTime.UtcNow; + logger.Log(); + var after = DateTime.UtcNow; + + var record = exportedItems[0]; + + // ObservedTimestamp is set by the SDK to when the log was captured. + Assert.InRange(record.ObservedTimestamp, before, after); + Assert.Equal(DateTimeKind.Utc, record.ObservedTimestamp.Kind); + + // Verify the setter converts local time to UTC. + var localNow = DateTime.Now; + record.ObservedTimestamp = localNow; + Assert.Equal(DateTimeKind.Utc, record.ObservedTimestamp.Kind); + Assert.Equal(localNow.ToUniversalTime(), record.ObservedTimestamp); + } + private static ILoggerFactory InitializeLoggerFactory(out List exportedItems, Action? configure = null) { var items = exportedItems = [];