diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md
index 8f6f72a1c33..233eb8147e7 100644
--- a/src/OpenTelemetry.Api/CHANGELOG.md
+++ b/src/OpenTelemetry.Api/CHANGELOG.md
@@ -17,6 +17,14 @@ Notes](../../RELEASENOTES.md).
instead of discarding the entire `tracestate` header.
([#7309](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7309))
+* **Breaking change** (pre-release only versions): The default value of
+ the `Timestamp` property on `LogRecordData` has changed from `DateTime.UtcNow`
+ to `DateTime.MinValue`. `DateTime.MinValue` represents an unset timestamp as
+ defined by the OpenTelemetry specification. Callers of the [Logs API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/api.md)
+ who relied on the timestamp being populated automatically must now set
+ `Timestamp = DateTime.UtcNow` explicitly on their `LogRecordData` instance.
+ ([#7045](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7045))
+
## 1.15.3
Released 2026-Apr-21
diff --git a/src/OpenTelemetry.Api/Logs/LogRecordData.cs b/src/OpenTelemetry.Api/Logs/LogRecordData.cs
index 0889ce0b6fb..8084f615f47 100644
--- a/src/OpenTelemetry.Api/Logs/LogRecordData.cs
+++ b/src/OpenTelemetry.Api/Logs/LogRecordData.cs
@@ -26,7 +26,7 @@ namespace OpenTelemetry.Logs;
struct LogRecordData
#pragma warning restore CA1815 // Override equals and operator equals on value types
{
- internal DateTime TimestampBacking = DateTime.UtcNow;
+ internal DateTime TimestampBacking = DateTime.MinValue;
///
/// Initializes a new instance of the struct.
@@ -35,7 +35,7 @@ struct LogRecordData
/// Notes:
///
/// - The property is initialized to automatically.
+ /// cref="DateTime.MinValue"/> automatically.
/// - The , , and properties will be set using the instance.
@@ -51,7 +51,9 @@ public LogRecordData()
///
///
/// Note: The property is initialized to automatically.
+ /// cref="DateTime.MinValue"/> automatically, indicating the timestamp is
+ /// not set. Set explicitly to provide the time the
+ /// event occurred.
///
/// Optional used to populate
/// trace context properties (, ,
@@ -66,7 +68,9 @@ public LogRecordData(Activity? activity)
///
///
/// Note: The property is initialized to automatically.
+ /// cref="DateTime.MinValue"/> automatically, indicating the timestamp is
+ /// not set. Set explicitly to provide the time the
+ /// event occurred.
///
/// used to
/// populate trace context properties (,
///
- /// Note: If is set to a value with it will be automatically converted to
- /// UTC using .
+ /// Notes:
+ ///
+ /// - The default value is , which is
+ /// treated as "not set" per the OpenTelemetry specification. When
+ /// exported via OTLP this maps to time_unix_nano = 0 (unknown or
+ /// missing timestamp).
+ /// - If is set to a value with it will be automatically converted to UTC
+ /// using .
+ ///
///
public DateTime Timestamp
{
diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs
index 49f18cb646c..14c9d1b028d 100644
--- a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs
+++ b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs
@@ -1,6 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
+using System.Globalization;
using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
@@ -47,7 +48,8 @@ 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}");
+ var timestamp = logRecord.Timestamp;
+ this.WriteLine($"{"LogRecord.Timestamp:",-RightPaddingLength}{(timestamp == DateTime.MinValue ? "(not set)" : timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture))}");
if (logRecord.TraceId != default)
{
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs
index 8b7ca3e9b79..a0800057e57 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpLogSerializer.cs
@@ -179,9 +179,25 @@ internal static int WriteLogRecord(byte[] buffer, int writePosition, SdkLimitOpt
var logRecordLengthPosition = otlpTagWriterState.WritePosition;
otlpTagWriterState.WritePosition += ReserveSizeForLength;
- var timestamp = (ulong)logRecord.Timestamp.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);
+ var logTimestamp = logRecord.Timestamp;
+ ulong timeUnixNano;
+ ulong observedTimeUnixNano;
+ if (logTimestamp != DateTime.MinValue)
+ {
+ // Timestamp was explicitly set: use it for both fields.
+ timeUnixNano = (ulong)logTimestamp.ToUnixTimeNanoseconds();
+ observedTimeUnixNano = timeUnixNano;
+ }
+ else
+ {
+ // Timestamp not set -> time_unix_nano = 0 ("unknown or missing" per OTLP spec).
+ // observed_time_unix_nano MUST still be populated (proto spec requirement).
+ timeUnixNano = 0;
+ observedTimeUnixNano = (ulong)DateTime.UtcNow.ToUnixTimeNanoseconds();
+ }
+
+ otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Time_Unix_Nano, timeUnixNano);
+ otlpTagWriterState.WritePosition = ProtobufSerializer.WriteFixed64WithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Observed_Time_Unix_Nano, observedTimeUnixNano);
otlpTagWriterState.WritePosition = ProtobufSerializer.WriteEnumWithTag(otlpTagWriterState.Buffer, otlpTagWriterState.WritePosition, ProtobufOtlpLogFieldNumberConstants.LogRecord_Severity_Number, logRecord.Severity.HasValue ? (int)logRecord.Severity : 0);
diff --git a/src/OpenTelemetry/Logs/LogRecord.cs b/src/OpenTelemetry/Logs/LogRecord.cs
index f27261e6806..add7addf0a9 100644
--- a/src/OpenTelemetry/Logs/LogRecord.cs
+++ b/src/OpenTelemetry/Logs/LogRecord.cs
@@ -104,9 +104,14 @@ internal enum LogRecordSource
/// Gets or sets the log timestamp.
///
///
- /// Note: If is set to a value with it will be automatically converted to
- /// UTC using .
+ /// Notes:
+ ///
+ /// - The default value is , which is
+ /// treated as "not set" per the OpenTelemetry specification.
+ /// - If is set to a value with it will be automatically converted to UTC
+ /// using .
+ ///
///
public DateTime Timestamp
{
diff --git a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs
index 1b515636de7..87b06f711e5 100644
--- a/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs
+++ b/test/OpenTelemetry.Api.Tests/Logs/LogRecordDataTests.cs
@@ -85,10 +85,8 @@ public void ActivityContextConstructorTest()
[Fact]
public void TimestampTest()
{
- var nowUtc = DateTime.UtcNow;
-
var record = new LogRecordData();
- Assert.True(record.Timestamp >= nowUtc);
+ Assert.Equal(DateTime.MinValue, record.Timestamp);
record = default;
Assert.Equal(DateTime.MinValue, record.Timestamp);
diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs
index 3d13e47a527..d36197af173 100644
--- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs
+++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpLogExporterTests.cs
@@ -460,6 +460,43 @@ public void CheckToOtlpLogRecordTimestamps()
Assert.True(otlpLogRecord.ObservedTimeUnixNano > 0);
}
+ [Fact]
+ public void CheckToOtlpLogRecordTimestamps_BridgeApi_UnsetTimestamp()
+ {
+ // When the Bridge API caller leaves Timestamp as DateTime.MinValue
+ // (not set), the serializer must write:
+ // time_unix_nano = 0 (unknown/missing per OTLP spec)
+ // observed_time_unix_nano >= the time of observation (MUST be set per OTLP spec)
+ var logRecords = new List();
+ using var loggerProvider = Sdk.CreateLoggerProviderBuilder()
+ .AddInMemoryExporter(logRecords)
+ .Build();
+
+ var bridgeLogger = loggerProvider.GetLogger("OtlpLogExporterTests");
+
+ // Capture a lower-bound for the observation timestamp before emitting.
+ var beforeEmitUtc = DateTime.UtcNow;
+
+ // Emit with default LogRecordData -- Timestamp stays DateTime.MinValue.
+ bridgeLogger.EmitLog(new LogRecordData());
+
+ Assert.Single(logRecords);
+ Assert.Equal(DateTime.MinValue, logRecords[0].Timestamp);
+
+ var otlpLogRecord = ToOtlpLogs(DefaultSdkLimitOptions, new ExperimentalOptions(), logRecords[0]);
+
+ Assert.NotNull(otlpLogRecord);
+
+ // time_unix_nano must be 0 -- "unknown or missing" per OTLP spec.
+ Assert.Equal(0UL, otlpLogRecord.TimeUnixNano);
+
+ // observed_time_unix_nano must be >= the moment we captured before emitting.
+ var beforeEmitNano = (ulong)new DateTimeOffset(beforeEmitUtc).ToUnixTimeNanoseconds();
+ Assert.True(
+ otlpLogRecord.ObservedTimeUnixNano >= beforeEmitNano,
+ $"ObservedTimeUnixNano ({otlpLogRecord.ObservedTimeUnixNano}) should be >= beforeEmitNano ({beforeEmitNano})");
+ }
+
[Fact]
public void CheckToOtlpLogRecordTraceIdSpanIdFlagWithNoActivity()
{