diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h index 333c01df8a..961501ff19 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h @@ -176,6 +176,7 @@ class BatchSpanProcessor : public SpanProcessor const size_t max_queue_size_; const std::chrono::milliseconds schedule_delay_millis_; const size_t max_export_batch_size_; + const bool export_unsampled_spans_; /* The buffer/queue to which the ended spans are added */ opentelemetry::sdk::common::CircularBuffer buffer_; diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h index ac1052383b..d7d92a4d3b 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h @@ -33,6 +33,15 @@ struct BatchSpanProcessorOptions * equal to max_queue_size. */ size_t max_export_batch_size = 512; + + /** + * Whether to export unsampled but recording spans. + * By default, only sampled spans (Decision::RECORD_AND_SAMPLE) are exported to maintain + * OpenTelemetry specification compliance. + * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported, + * which intentionally violates the OpenTelemetry specification. + */ + bool export_unsampled_spans = false; }; } // namespace trace diff --git a/sdk/include/opentelemetry/sdk/trace/simple_processor.h b/sdk/include/opentelemetry/sdk/trace/simple_processor.h index 6a562a8f0a..b47528bfae 100644 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor.h @@ -15,6 +15,8 @@ #include "opentelemetry/sdk/trace/exporter.h" #include "opentelemetry/sdk/trace/processor.h" #include "opentelemetry/sdk/trace/recordable.h" +#include "opentelemetry/sdk/trace/simple_processor_options.h" +#include "opentelemetry/sdk/trace/span_data.h" #include "opentelemetry/trace/span_context.h" #include "opentelemetry/version.h" @@ -40,7 +42,17 @@ class SimpleSpanProcessor : public SpanProcessor * @param exporter the exporter used by the span processor */ explicit SimpleSpanProcessor(std::unique_ptr &&exporter) noexcept - : exporter_(std::move(exporter)) + : exporter_(std::move(exporter)), export_unsampled_spans_(false) + {} + + /** + * Initialize a simple span processor with options. + * @param exporter the exporter used by the span processor + * @param options the processor options + */ + explicit SimpleSpanProcessor(std::unique_ptr &&exporter, + const SimpleSpanProcessorOptions &options) noexcept + : exporter_(std::move(exporter)), export_unsampled_spans_(options.export_unsampled_spans) {} std::unique_ptr MakeRecordable() noexcept override @@ -54,6 +66,22 @@ class SimpleSpanProcessor : public SpanProcessor void OnEnd(std::unique_ptr &&span) noexcept override { + // Check if we should export this span based on sampling status + auto *span_data = static_cast(span.get()); + const auto &span_context = span_data->GetSpanContext(); + + // For backward compatibility: always export spans with invalid context (e.g., test spans) + // For valid contexts: export sampled spans or unsampled spans if export_unsampled_spans is + // enabled + bool should_export = + !span_context.IsValid() || span_context.IsSampled() || export_unsampled_spans_; + + if (!should_export) + { + // Drop unsampled spans if export_unsampled_spans is not enabled + return; + } + nostd::span> batch(&span, 1); const std::lock_guard locked(lock_); if (exporter_->Export(batch) == sdk::common::ExportResult::kFailure) @@ -96,6 +124,7 @@ class SimpleSpanProcessor : public SpanProcessor private: std::unique_ptr exporter_; + const bool export_unsampled_spans_; opentelemetry::common::SpinLockMutex lock_; #if defined(__cpp_lib_atomic_value_initialization) && \ __cpp_lib_atomic_value_initialization >= 201911L diff --git a/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h b/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h new file mode 100644 index 0000000000..6ee911e908 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ + +namespace trace +{ + +/** + * Struct to hold simple SpanProcessor options. + */ +struct SimpleSpanProcessorOptions +{ + /** + * Whether to export unsampled but recording spans. + * By default, only sampled spans (Decision::RECORD_AND_SAMPLE) are exported to maintain + * OpenTelemetry specification compliance. + * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported, + * which intentionally violates the OpenTelemetry specification. + */ + bool export_unsampled_spans = false; +}; + +} // namespace trace +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index ad93eae7ae..95142f4b3d 100644 --- a/sdk/src/trace/batch_span_processor.cc +++ b/sdk/src/trace/batch_span_processor.cc @@ -25,6 +25,7 @@ #include "opentelemetry/sdk/trace/exporter.h" #include "opentelemetry/sdk/trace/processor.h" #include "opentelemetry/sdk/trace/recordable.h" +#include "opentelemetry/sdk/trace/span_data.h" #include "opentelemetry/version.h" #ifdef ENABLE_THREAD_INSTRUMENTATION_PREVIEW @@ -47,6 +48,7 @@ BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr &&exporter, max_queue_size_(options.max_queue_size), schedule_delay_millis_(options.schedule_delay_millis), max_export_batch_size_(options.max_export_batch_size), + export_unsampled_spans_(options.export_unsampled_spans), buffer_(max_queue_size_), synchronization_data_(std::make_shared()), worker_thread_instrumentation_(nullptr), @@ -63,6 +65,7 @@ BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr &&exporter, max_queue_size_(options.max_queue_size), schedule_delay_millis_(options.schedule_delay_millis), max_export_batch_size_(options.max_export_batch_size), + export_unsampled_spans_(options.export_unsampled_spans), buffer_(max_queue_size_), synchronization_data_(std::make_shared()), worker_thread_instrumentation_(runtime_options.thread_instrumentation), @@ -89,6 +92,22 @@ void BatchSpanProcessor::OnEnd(std::unique_ptr &&span) noexcept return; } + // Check if we should export this span based on sampling status + auto *span_data = static_cast(span.get()); + const auto &span_context = span_data->GetSpanContext(); + + // For backward compatibility: always export spans with invalid context (e.g., test spans) + // For valid contexts: export sampled spans or unsampled spans if export_unsampled_spans is + // enabled + bool should_export = + !span_context.IsValid() || span_context.IsSampled() || export_unsampled_spans_; + + if (!should_export) + { + // Drop unsampled spans if export_unsampled_spans is not enabled + return; + } + if (buffer_.Add(std::move(span)) == false) { OTEL_INTERNAL_LOG_WARN("BatchSpanProcessor queue is full - dropping span."); diff --git a/sdk/test/trace/CMakeLists.txt b/sdk/test/trace/CMakeLists.txt index de8dee2340..9a560fd6e0 100644 --- a/sdk/test/trace/CMakeLists.txt +++ b/sdk/test/trace/CMakeLists.txt @@ -13,7 +13,8 @@ foreach( parent_sampler_test trace_id_ratio_sampler_test batch_span_processor_test - tracer_config_test) + tracer_config_test + unsampled_span_processor_test) add_executable(${testname} "${testname}.cc") target_link_libraries( ${testname} diff --git a/sdk/test/trace/unsampled_span_processor_test.cc b/sdk/test/trace/unsampled_span_processor_test.cc new file mode 100644 index 0000000000..8313a927cf --- /dev/null +++ b/sdk/test/trace/unsampled_span_processor_test.cc @@ -0,0 +1,302 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/simple_processor_options.h" +#include "opentelemetry/sdk/trace/span_data.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/trace_flags.h" +#include "opentelemetry/trace/trace_id.h" + +using namespace opentelemetry::sdk::trace; +using namespace opentelemetry::trace; + +namespace +{ + +/** + * Mock span exporter for testing + */ +class MockSpanExporter : public SpanExporter +{ +public: + explicit MockSpanExporter(std::vector> *spans) : spans_(spans) {} + + std::unique_ptr MakeRecordable() noexcept override + { + return std::unique_ptr(new SpanData); + } + + opentelemetry::sdk::common::ExportResult Export( + const opentelemetry::nostd::span> &recordables) noexcept override + { + for (auto &recordable : recordables) + { + auto span = std::unique_ptr(static_cast(recordable.release())); + spans_->push_back(std::move(span)); + } + return opentelemetry::sdk::common::ExportResult::kSuccess; + } + + bool ForceFlush(std::chrono::microseconds) noexcept override { return true; } + + bool Shutdown(std::chrono::microseconds) noexcept override { return true; } + +private: + std::vector> *spans_; +}; + +/** + * Create a span with specific sampling status + */ +std::unique_ptr CreateTestSpan(bool sampled, bool valid_context = true) +{ + auto span = std::make_unique(); + span->SetName("test_span"); + + if (valid_context) + { + TraceFlags flags(sampled ? TraceFlags::kIsSampled : 0); + + // Create valid trace id and span id using arrays + uint8_t trace_id_bytes[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + uint8_t span_id_bytes[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + + TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; + SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; + + SpanContext context(trace_id, span_id, flags, false); + span->SetIdentity(context, SpanId()); + } + // If valid_context is false, we leave the default invalid context + + return span; +} + +/** + * Test BatchSpanProcessor with export_unsampled_spans flag + */ +class BatchSpanProcessorUnsampledTest : public testing::Test +{ +public: + void SetUp() override {} + + void TearDown() override + { + if (processor_) + { + processor_->Shutdown(); + } + } + +protected: + std::vector> spans_; + std::shared_ptr processor_; +}; + +TEST_F(BatchSpanProcessorUnsampledTest, DefaultBehaviorDropsUnsampledSpans) +{ + // Default options should not export unsampled spans + BatchSpanProcessorOptions options; + EXPECT_FALSE(options.export_unsampled_spans); + + processor_ = std::make_shared( + std::unique_ptr(new MockSpanExporter(&spans_)), options); + + // Create a sampled span and an unsampled span with valid contexts + auto sampled_span = CreateTestSpan(true, true); + auto unsampled_span = CreateTestSpan(false, true); + + // Process the spans + std::unique_ptr sampled_recordable(sampled_span.release()); + std::unique_ptr unsampled_recordable(unsampled_span.release()); + + processor_->OnEnd(std::move(sampled_recordable)); + processor_->OnEnd(std::move(unsampled_recordable)); + + // Force flush to export spans + processor_->ForceFlush(); + + // Should only export the sampled span + EXPECT_EQ(1, spans_.size()); + EXPECT_TRUE(spans_[0]->GetSpanContext().IsSampled()); +} + +TEST_F(BatchSpanProcessorUnsampledTest, EnabledFlagExportsUnsampledSpans) +{ + // Enable exporting unsampled spans + BatchSpanProcessorOptions options; + options.export_unsampled_spans = true; + + processor_ = std::make_shared( + std::unique_ptr(new MockSpanExporter(&spans_)), options); + + // Create a sampled span and an unsampled span with valid contexts + auto sampled_span = CreateTestSpan(true, true); + auto unsampled_span = CreateTestSpan(false, true); + + // Process the spans + std::unique_ptr sampled_recordable(sampled_span.release()); + std::unique_ptr unsampled_recordable(unsampled_span.release()); + + processor_->OnEnd(std::move(sampled_recordable)); + processor_->OnEnd(std::move(unsampled_recordable)); + + // Force flush to export spans + processor_->ForceFlush(); + + // Should export both spans + EXPECT_EQ(2, spans_.size()); + + // Check that we have one sampled and one unsampled span + bool has_sampled = false; + bool has_unsampled = false; + for (const auto &span : spans_) + { + if (span->GetSpanContext().IsSampled()) + { + has_sampled = true; + } + else + { + has_unsampled = true; + } + } + EXPECT_TRUE(has_sampled); + EXPECT_TRUE(has_unsampled); +} + +TEST_F(BatchSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) +{ + // Default options - should not export valid unsampled spans but should export invalid context + // spans + BatchSpanProcessorOptions options; + options.export_unsampled_spans = false; + + processor_ = std::make_shared( + std::unique_ptr(new MockSpanExporter(&spans_)), options); + + // Create spans with invalid context (like test spans) + auto invalid_span1 = CreateTestSpan(false, false); + auto invalid_span2 = CreateTestSpan(true, false); + + // Process the spans + std::unique_ptr span1_recordable(invalid_span1.release()); + std::unique_ptr span2_recordable(invalid_span2.release()); + + processor_->OnEnd(std::move(span1_recordable)); + processor_->OnEnd(std::move(span2_recordable)); + + // Force flush to export spans + processor_->ForceFlush(); + + // Should export both spans for backward compatibility + EXPECT_EQ(2, spans_.size()); +} + +/** + * Test SimpleSpanProcessor with export_unsampled_spans flag + */ +class SimpleSpanProcessorUnsampledTest : public testing::Test +{ +protected: + std::vector> spans_; +}; + +TEST_F(SimpleSpanProcessorUnsampledTest, DefaultBehaviorDropsUnsampledSpans) +{ + // Default constructor should not export unsampled spans + auto processor = std::make_unique( + std::unique_ptr(new MockSpanExporter(&spans_))); + + // Create a sampled span and an unsampled span with valid contexts + auto sampled_span = CreateTestSpan(true, true); + auto unsampled_span = CreateTestSpan(false, true); + + // Process the spans + std::unique_ptr sampled_recordable(sampled_span.release()); + std::unique_ptr unsampled_recordable(unsampled_span.release()); + + processor->OnEnd(std::move(sampled_recordable)); + processor->OnEnd(std::move(unsampled_recordable)); + + // Should only export the sampled span + EXPECT_EQ(1, spans_.size()); + EXPECT_TRUE(spans_[0]->GetSpanContext().IsSampled()); +} + +TEST_F(SimpleSpanProcessorUnsampledTest, EnabledFlagExportsUnsampledSpans) +{ + // Enable exporting unsampled spans + SimpleSpanProcessorOptions options; + options.export_unsampled_spans = true; + + auto processor = std::make_unique( + std::unique_ptr(new MockSpanExporter(&spans_)), options); + + // Create a sampled span and an unsampled span with valid contexts + auto sampled_span = CreateTestSpan(true, true); + auto unsampled_span = CreateTestSpan(false, true); + + // Process the spans + std::unique_ptr sampled_recordable(sampled_span.release()); + std::unique_ptr unsampled_recordable(unsampled_span.release()); + + processor->OnEnd(std::move(sampled_recordable)); + processor->OnEnd(std::move(unsampled_recordable)); + + // Should export both spans + EXPECT_EQ(2, spans_.size()); + + // Check that we have one sampled and one unsampled span + bool has_sampled = false; + bool has_unsampled = false; + for (const auto &span : spans_) + { + if (span->GetSpanContext().IsSampled()) + { + has_sampled = true; + } + else + { + has_unsampled = true; + } + } + EXPECT_TRUE(has_sampled); + EXPECT_TRUE(has_unsampled); +} + +TEST_F(SimpleSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) +{ + // Default options - should not export valid unsampled spans but should export invalid context + // spans + SimpleSpanProcessorOptions options; + options.export_unsampled_spans = false; + + auto processor = std::make_unique( + std::unique_ptr(new MockSpanExporter(&spans_)), options); + + // Create spans with invalid context (like test spans) + auto invalid_span1 = CreateTestSpan(false, false); + auto invalid_span2 = CreateTestSpan(true, false); + + // Process the spans + std::unique_ptr span1_recordable(invalid_span1.release()); + std::unique_ptr span2_recordable(invalid_span2.release()); + + processor->OnEnd(std::move(span1_recordable)); + processor->OnEnd(std::move(span2_recordable)); + + // Should export both spans for backward compatibility + EXPECT_EQ(2, spans_.size()); +} + +} // namespace \ No newline at end of file