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 = [];