Skip to content

Commit 6795dc4

Browse files
authored
[ETW] Fix ETW Log Exporter: incorrect timestamp, traceId, and spanId (open-telemetry#3836)
1 parent b49bc4e commit 6795dc4

File tree

3 files changed

+161
-2
lines changed

3 files changed

+161
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Increment the:
1515

1616
## [Unreleased]
1717

18+
* [ETW] Fix ETW Log Exporter: incorrect timestamp, traceId, and spanId
19+
[#3836](https://github.com/open-telemetry/opentelemetry-cpp/pull/3836)
20+
1821
* [ETW] Fix infinite loop in ETWProvider::close
1922
[#3827](https://github.com/open-telemetry/opentelemetry-cpp/pull/3827)
2023

exporters/etw/include/opentelemetry/exporters/etw/etw_logger.h

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
#include "opentelemetry/trace/span_id.h"
3232
#include "opentelemetry/trace/trace_id.h"
3333

34+
#include "opentelemetry/context/runtime_context.h"
35+
#include "opentelemetry/trace/span.h"
36+
#include "opentelemetry/trace/span_context.h"
37+
#include "opentelemetry/trace/span_metadata.h"
38+
3439
#include "opentelemetry/exporters/etw/etw_config.h"
3540
#include "opentelemetry/exporters/etw/etw_fields.h"
3641
#include "opentelemetry/exporters/etw/etw_properties.h"
@@ -121,7 +126,7 @@ class LogRecord : public opentelemetry::logs::LogRecord
121126

122127
std::unordered_map<std::string, opentelemetry::common::AttributeValue> attributes_map_;
123128
opentelemetry::common::AttributeValue body_ = opentelemetry::nostd::string_view();
124-
opentelemetry::common::SystemTimestamp timestamp_;
129+
opentelemetry::common::SystemTimestamp timestamp_; // unset (epoch) by default
125130
opentelemetry::common::SystemTimestamp observed_timestamp_ = std::chrono::system_clock::now();
126131

127132
opentelemetry::trace::TraceId trace_id_;
@@ -194,7 +199,44 @@ class Logger : public opentelemetry::logs::Logger
194199

195200
nostd::unique_ptr<opentelemetry::logs::LogRecord> CreateLogRecord() noexcept
196201
{
197-
return nostd::unique_ptr<opentelemetry::logs::LogRecord>(new LogRecord());
202+
nostd::unique_ptr<LogRecord> log_record(new LogRecord());
203+
204+
// Populate trace context from the current active span (if any)
205+
if (opentelemetry::context::RuntimeContext::GetCurrent().HasKey(opentelemetry::trace::kSpanKey))
206+
{
207+
opentelemetry::context::ContextValue context_value =
208+
opentelemetry::context::RuntimeContext::GetCurrent().GetValue(
209+
opentelemetry::trace::kSpanKey);
210+
if (opentelemetry::nostd::holds_alternative<
211+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(context_value))
212+
{
213+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span> &span =
214+
opentelemetry::nostd::get<opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>>(
215+
context_value);
216+
if (span)
217+
{
218+
log_record->SetTraceId(span->GetContext().trace_id());
219+
log_record->SetSpanId(span->GetContext().span_id());
220+
log_record->SetTraceFlags(span->GetContext().trace_flags());
221+
}
222+
}
223+
else if (opentelemetry::nostd::holds_alternative<
224+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::SpanContext>>(
225+
context_value))
226+
{
227+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::SpanContext> &span_context =
228+
opentelemetry::nostd::get<
229+
opentelemetry::nostd::shared_ptr<opentelemetry::trace::SpanContext>>(context_value);
230+
if (span_context)
231+
{
232+
log_record->SetTraceId(span_context->trace_id());
233+
log_record->SetSpanId(span_context->span_id());
234+
log_record->SetTraceFlags(span_context->trace_flags());
235+
}
236+
}
237+
}
238+
239+
return nostd::unique_ptr<opentelemetry::logs::LogRecord>(log_record.release());
198240
}
199241

200242
using opentelemetry::logs::Logger::EmitLogRecord;
@@ -305,6 +347,11 @@ class Logger : public opentelemetry::logs::Logger
305347
}
306348
evt[ETW_FIELD_PAYLOAD_NAME] = std::string(name.data(), name.size());
307349
std::chrono::system_clock::time_point ts = timestamp;
350+
// Per OTel Log Data Model: if timestamp is unset (epoch), fall back to observed time (now)
351+
if (ts.time_since_epoch().count() == 0)
352+
{
353+
ts = std::chrono::system_clock::now();
354+
}
308355
int64_t tsNs =
309356
std::chrono::duration_cast<std::chrono::nanoseconds>(ts.time_since_epoch()).count();
310357
evt[ETW_FIELD_TIMESTAMP] = utils::formatUtcTimestampNsAsISO8601(tsNs);

exporters/etw/test/etw_logger_test.cc

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# define OPENTELEMETRY_ATTRIBUTE_TIMESTAMP_PREVIEW
1212

1313
# include "opentelemetry/exporters/etw/etw_logger_exporter.h"
14+
# include "opentelemetry/exporters/etw/etw_tracer_exporter.h"
1415
# include "opentelemetry/sdk/trace/simple_processor.h"
1516

1617
using namespace OPENTELEMETRY_NAMESPACE;
@@ -195,4 +196,112 @@ TEST(ETWLogger, LoggerCheckWithTimestampAttributes)
195196
logger->Debug("This is a debug log body", opentelemetry::common::MakeAttributes(attribs)));
196197
}
197198

199+
/**
200+
* @brief Test that LogRecord created within an active span context
201+
* inherits TraceId, SpanId, and TraceFlags from the current span.
202+
*
203+
* This test verifies the fix for issue #3830 where traceId and spanId
204+
* were incorrectly reported as all zeros.
205+
*/
206+
TEST(ETWLogger, LoggerInheritsTraceContextFromActiveSpan)
207+
{
208+
std::string providerName = kGlobalProviderName;
209+
210+
// Create tracer and logger providers
211+
exporter::etw::TracerProvider tp;
212+
exporter::etw::LoggerProvider lp;
213+
214+
auto tracer = tp.GetTracer(providerName);
215+
auto logger = lp.GetLogger(providerName, "test");
216+
217+
// Create a span and set it as the active span
218+
auto span = tracer->StartSpan("TestSpan");
219+
auto scope = tracer->WithActiveSpan(span);
220+
221+
// Get the span's trace context for verification
222+
auto span_context = span->GetContext();
223+
224+
// Create a log record while the span is active
225+
auto log_record = logger->CreateLogRecord();
226+
ASSERT_NE(log_record, nullptr);
227+
228+
// Cast to ETW LogRecord to access trace context
229+
auto *etw_record = static_cast<exporter::etw::LogRecord *>(log_record.get());
230+
231+
// Verify TraceId matches the active span's TraceId
232+
EXPECT_EQ(etw_record->GetTraceId(), span_context.trace_id());
233+
234+
// Verify SpanId matches the active span's SpanId
235+
EXPECT_EQ(etw_record->GetSpanId(), span_context.span_id());
236+
237+
// Verify TraceFlags match
238+
EXPECT_EQ(etw_record->GetTraceFlags(), span_context.trace_flags());
239+
240+
// Emit the log (should not throw)
241+
EXPECT_NO_THROW(logger->EmitLogRecord(std::move(log_record)));
242+
243+
span->End();
244+
}
245+
246+
/**
247+
* @brief Test that LogRecord created without an active span context
248+
* has default (invalid/zero) TraceId and SpanId.
249+
*/
250+
TEST(ETWLogger, LoggerWithoutActiveSpanHasDefaultTraceContext)
251+
{
252+
std::string providerName = kGlobalProviderName;
253+
exporter::etw::LoggerProvider lp;
254+
255+
auto logger = lp.GetLogger(providerName, "test");
256+
257+
// Create a log record without any active span
258+
auto log_record = logger->CreateLogRecord();
259+
ASSERT_NE(log_record, nullptr);
260+
261+
auto *etw_record = static_cast<exporter::etw::LogRecord *>(log_record.get());
262+
263+
// TraceId and SpanId should be invalid (all zeros) when no span is active
264+
EXPECT_FALSE(etw_record->GetTraceId().IsValid());
265+
EXPECT_FALSE(etw_record->GetSpanId().IsValid());
266+
267+
EXPECT_NO_THROW(logger->EmitLogRecord(std::move(log_record)));
268+
}
269+
270+
/**
271+
* @brief Test that LogRecord timestamp defaults to a valid time (not epoch).
272+
*
273+
* This test verifies the fix for issue #3830 where timestamp was
274+
* incorrectly reported as 1970-01-01 (epoch).
275+
*/
276+
TEST(ETWLogger, LoggerTimestampIsNotEpoch)
277+
{
278+
std::string providerName = kGlobalProviderName;
279+
exporter::etw::LoggerProvider lp;
280+
281+
auto logger = lp.GetLogger(providerName, "test");
282+
283+
// Capture time before creating log record
284+
auto before = std::chrono::system_clock::now();
285+
286+
auto log_record = logger->CreateLogRecord();
287+
ASSERT_NE(log_record, nullptr);
288+
289+
// Capture time after creating log record
290+
auto after = std::chrono::system_clock::now();
291+
292+
auto *etw_record = static_cast<exporter::etw::LogRecord *>(log_record.get());
293+
294+
// Get the observed timestamp (which should be initialized to now())
295+
std::chrono::system_clock::time_point observed_ts = etw_record->GetObservedTimestamp();
296+
297+
// Verify observed timestamp is not epoch (1970-01-01)
298+
EXPECT_GT(observed_ts.time_since_epoch().count(), 0);
299+
300+
// Verify observed timestamp is within the expected range
301+
EXPECT_GE(observed_ts, before);
302+
EXPECT_LE(observed_ts, after);
303+
304+
EXPECT_NO_THROW(logger->EmitLogRecord(std::move(log_record)));
305+
}
306+
198307
#endif // _WIN32

0 commit comments

Comments
 (0)