From 06f1fdfb54e98d12cf5fd1a99de06824d8c9463f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 04:52:40 +0000 Subject: [PATCH 01/10] Initial plan From 745aec7253d7a39efa41003a5dc274487af9f7e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 05:07:43 +0000 Subject: [PATCH 02/10] Implement export_unsampled_spans feature for span processors Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- .../sdk/trace/batch_span_processor.h | 1 + .../sdk/trace/batch_span_processor_options.h | 7 + .../sdk/trace/simple_processor.h | 31 +- .../sdk/trace/simple_processor_options.h | 30 ++ sdk/src/trace/batch_span_processor.cc | 19 ++ sdk/test/trace/CMakeLists.txt | 3 +- .../trace/unsampled_span_processor_test.cc | 301 ++++++++++++++++++ 7 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 sdk/include/opentelemetry/sdk/trace/simple_processor_options.h create mode 100644 sdk/test/trace/unsampled_span_processor_test.cc diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h index 3a78d6f33c..d184fff4d6 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h @@ -164,6 +164,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..3f73677f67 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,13 @@ 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. + * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported. + */ + 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 ce6d378b79..b85055caff 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) @@ -89,6 +117,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..0886434f06 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h @@ -0,0 +1,30 @@ +// 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. + * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported. + */ + bool export_unsampled_spans = false; +}; + +} // namespace trace +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE \ No newline at end of file diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index 5ec2376656..8803989078 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..ff26085679 --- /dev/null +++ b/sdk/test/trace/unsampled_span_processor_test.cc @@ -0,0 +1,301 @@ +// 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/trace_flags.h" +#include "opentelemetry/trace/trace_id.h" +#include "opentelemetry/trace/span_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 From 761e20bc0381fb3b918fe3b6c423ae1ed49e373c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 05:09:43 +0000 Subject: [PATCH 03/10] Add documentation for export_unsampled_spans feature Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- docs/export-unsampled-spans.md | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/export-unsampled-spans.md diff --git a/docs/export-unsampled-spans.md b/docs/export-unsampled-spans.md new file mode 100644 index 0000000000..df63c537bd --- /dev/null +++ b/docs/export-unsampled-spans.md @@ -0,0 +1,60 @@ +# Export Unsampled Spans Feature + +The OpenTelemetry C++ SDK supports an opt-in feature to export unsampled but recording spans from trace processors. This allows collectors to see the full request volume, which is useful for calculating accurate metrics and performing tail-based sampling. + +## Overview + +By default, the C++ SDK only exports sampled spans (`Decision::RECORD_AND_SAMPLE`). With this feature enabled, spans with `Decision::RECORD_ONLY` (unsampled but recording) will also be exported. + +## Usage + +### BatchSpanProcessor + +```cpp +#include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" + +// Configure BatchSpanProcessor to export unsampled spans +opentelemetry::sdk::trace::BatchSpanProcessorOptions options; +options.export_unsampled_spans = true; // Default is false + +auto processor = std::make_unique( + std::move(exporter), options); +``` + +### SimpleSpanProcessor + +```cpp +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/simple_processor_options.h" + +// Configure SimpleSpanProcessor to export unsampled spans +opentelemetry::sdk::trace::SimpleSpanProcessorOptions options; +options.export_unsampled_spans = true; // Default is false + +auto processor = std::make_unique( + std::move(exporter), options); +``` + +## Behavior + +When `export_unsampled_spans` is: + +- **false** (default): Only sampled spans are exported +- **true**: Both sampled and unsampled recording spans are exported + +## Sampling Decisions + +The feature respects the sampling decisions made during span creation: + +- `Decision::DROP`: Spans are not recorded and never reach the processor (not affected) +- `Decision::RECORD_ONLY`: Spans are recorded but not sampled by default (affected by this feature) +- `Decision::RECORD_AND_SAMPLE`: Spans are recorded and sampled (always exported) + +## Backward Compatibility + +This feature maintains full backward compatibility: + +- Default behavior is unchanged (only sampled spans are exported) +- Existing constructors continue to work as before +- Test spans with invalid contexts are always exported for compatibility \ No newline at end of file From 1248f765de4dc45cb217238385a434e1f9b31a3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 Aug 2025 05:11:11 +0000 Subject: [PATCH 04/10] Add demo example and finalize export_unsampled_spans implementation Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- docs/export-unsampled-spans.md | 6 +- examples/unsampled_spans_demo.cc | 126 +++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 examples/unsampled_spans_demo.cc diff --git a/docs/export-unsampled-spans.md b/docs/export-unsampled-spans.md index df63c537bd..81cd38e7ab 100644 --- a/docs/export-unsampled-spans.md +++ b/docs/export-unsampled-spans.md @@ -57,4 +57,8 @@ This feature maintains full backward compatibility: - Default behavior is unchanged (only sampled spans are exported) - Existing constructors continue to work as before -- Test spans with invalid contexts are always exported for compatibility \ No newline at end of file +- Test spans with invalid contexts are always exported for compatibility + +## Example + +See [examples/unsampled_spans_demo.cc](../examples/unsampled_spans_demo.cc) for a complete working example demonstrating the feature. \ No newline at end of file diff --git a/examples/unsampled_spans_demo.cc b/examples/unsampled_spans_demo.cc new file mode 100644 index 0000000000..e9b410fd95 --- /dev/null +++ b/examples/unsampled_spans_demo.cc @@ -0,0 +1,126 @@ +// Example demonstrating export_unsampled_spans feature +// This example shows how to configure span processors to export unsampled spans + +#include +#include + +#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/trace_flags.h" + +namespace trace_sdk = opentelemetry::sdk::trace; +namespace trace_api = opentelemetry::trace; + +// Simple mock exporter for demonstration +class MockExporter : public trace_sdk::SpanExporter { +public: + explicit MockExporter(const std::string& name) : name_(name) {} + + std::unique_ptr MakeRecordable() noexcept override { + return std::make_unique(); + } + + opentelemetry::sdk::common::ExportResult Export( + const opentelemetry::nostd::span>& recordables) noexcept override { + std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl; + 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::string name_; +}; + +// Create a test span with specific sampling status +std::unique_ptr CreateTestSpan(bool sampled) { + auto span = std::make_unique(); + span->SetName("test_span"); + + // Set up valid context with proper sampling + trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0); + 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}; + + trace_api::TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; + trace_api::SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; + + trace_api::SpanContext context(trace_id, span_id, flags, false); + span->SetIdentity(context, trace_api::SpanId()); + + return span; +} + +int main() { + std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl; + + // Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior) + { + std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl; + + trace_sdk::BatchSpanProcessorOptions options; + // export_unsampled_spans is false by default + + auto processor = std::make_unique( + std::make_unique("BatchProcessor-Default"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + processor->ForceFlush(); + std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl; + } + + // Example 2: BatchSpanProcessor with export_unsampled_spans enabled + { + std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" << std::endl; + + trace_sdk::BatchSpanProcessorOptions options; + options.export_unsampled_spans = true; // Enable exporting unsampled spans + + auto processor = std::make_unique( + std::make_unique("BatchProcessor-WithUnsampled"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + processor->ForceFlush(); + std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl; + } + + // Example 3: SimpleSpanProcessor with export_unsampled_spans enabled + { + std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" << std::endl; + + trace_sdk::SimpleSpanProcessorOptions options; + options.export_unsampled_spans = true; // Enable exporting unsampled spans + + auto processor = std::make_unique( + std::make_unique("SimpleProcessor-WithUnsampled"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl; + } + + std::cout << "Demo completed!" << std::endl; + return 0; +} \ No newline at end of file From cbee80d47f59d04ccb36ddc874045e34788de3ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:28:23 +0000 Subject: [PATCH 05/10] Fix copyright header in unsampled_spans_demo.cc Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- examples/unsampled_spans_demo.cc | 89 +++++++++++-------- .../sdk/trace/simple_processor.h | 14 +-- sdk/src/trace/batch_span_processor.cc | 14 +-- .../trace/unsampled_span_processor_test.cc | 81 ++++++++--------- 4 files changed, 105 insertions(+), 93 deletions(-) diff --git a/examples/unsampled_spans_demo.cc b/examples/unsampled_spans_demo.cc index e9b410fd95..5c0579ef74 100644 --- a/examples/unsampled_spans_demo.cc +++ b/examples/unsampled_spans_demo.cc @@ -1,3 +1,6 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + // Example demonstrating export_unsampled_spans feature // This example shows how to configure span processors to export unsampled spans @@ -16,111 +19,119 @@ namespace trace_sdk = opentelemetry::sdk::trace; namespace trace_api = opentelemetry::trace; // Simple mock exporter for demonstration -class MockExporter : public trace_sdk::SpanExporter { +class MockExporter : public trace_sdk::SpanExporter +{ public: - explicit MockExporter(const std::string& name) : name_(name) {} - - std::unique_ptr MakeRecordable() noexcept override { + explicit MockExporter(const std::string &name) : name_(name) {} + + std::unique_ptr MakeRecordable() noexcept override + { return std::make_unique(); } - + opentelemetry::sdk::common::ExportResult Export( - const opentelemetry::nostd::span>& recordables) noexcept override { + const opentelemetry::nostd::span> + &recordables) noexcept override + { std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl; 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::string name_; }; // Create a test span with specific sampling status -std::unique_ptr CreateTestSpan(bool sampled) { +std::unique_ptr CreateTestSpan(bool sampled) +{ auto span = std::make_unique(); span->SetName("test_span"); - + // Set up valid context with proper sampling trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0); 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}; - + uint8_t span_id_bytes[8] = {1, 2, 3, 4, 5, 6, 7, 8}; + trace_api::TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; trace_api::SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; - + trace_api::SpanContext context(trace_id, span_id, flags, false); span->SetIdentity(context, trace_api::SpanId()); - + return span; } -int main() { +int main() +{ std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl; - + // Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior) { std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl; - + trace_sdk::BatchSpanProcessorOptions options; // export_unsampled_spans is false by default - + auto processor = std::make_unique( std::make_unique("BatchProcessor-Default"), options); - + // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); + auto sampled_span = CreateTestSpan(true); auto unsampled_span = CreateTestSpan(false); - + processor->OnEnd(std::unique_ptr(sampled_span.release())); processor->OnEnd(std::unique_ptr(unsampled_span.release())); - + processor->ForceFlush(); std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl; } - + // Example 2: BatchSpanProcessor with export_unsampled_spans enabled { - std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" << std::endl; - + std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" + << std::endl; + trace_sdk::BatchSpanProcessorOptions options; options.export_unsampled_spans = true; // Enable exporting unsampled spans - + auto processor = std::make_unique( std::make_unique("BatchProcessor-WithUnsampled"), options); - + // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); + auto sampled_span = CreateTestSpan(true); auto unsampled_span = CreateTestSpan(false); - + processor->OnEnd(std::unique_ptr(sampled_span.release())); processor->OnEnd(std::unique_ptr(unsampled_span.release())); - + processor->ForceFlush(); std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl; } - + // Example 3: SimpleSpanProcessor with export_unsampled_spans enabled { - std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" << std::endl; - + std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" + << std::endl; + trace_sdk::SimpleSpanProcessorOptions options; options.export_unsampled_spans = true; // Enable exporting unsampled spans - + auto processor = std::make_unique( std::make_unique("SimpleProcessor-WithUnsampled"), options); - + // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); + auto sampled_span = CreateTestSpan(true); auto unsampled_span = CreateTestSpan(false); - + processor->OnEnd(std::unique_ptr(sampled_span.release())); processor->OnEnd(std::unique_ptr(unsampled_span.release())); - + std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl; } - + std::cout << "Demo completed!" << std::endl; return 0; } \ No newline at end of file diff --git a/sdk/include/opentelemetry/sdk/trace/simple_processor.h b/sdk/include/opentelemetry/sdk/trace/simple_processor.h index b85055caff..3332296d78 100644 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor.h @@ -67,15 +67,15 @@ 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()); + 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_; - + // 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 diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index 8803989078..078ac869cc 100644 --- a/sdk/src/trace/batch_span_processor.cc +++ b/sdk/src/trace/batch_span_processor.cc @@ -93,15 +93,15 @@ void BatchSpanProcessor::OnEnd(std::unique_ptr &&span) noexcept } // Check if we should export this span based on sampling status - auto *span_data = static_cast(span.get()); + 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_; - + // 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 diff --git a/sdk/test/trace/unsampled_span_processor_test.cc b/sdk/test/trace/unsampled_span_processor_test.cc index ff26085679..8313a927cf 100644 --- a/sdk/test/trace/unsampled_span_processor_test.cc +++ b/sdk/test/trace/unsampled_span_processor_test.cc @@ -12,9 +12,9 @@ #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" -#include "opentelemetry/trace/span_id.h" using namespace opentelemetry::sdk::trace; using namespace opentelemetry::trace; @@ -28,8 +28,7 @@ namespace class MockSpanExporter : public SpanExporter { public: - explicit MockSpanExporter(std::vector>* spans) - : spans_(spans) {} + explicit MockSpanExporter(std::vector> *spans) : spans_(spans) {} std::unique_ptr MakeRecordable() noexcept override { @@ -52,7 +51,7 @@ class MockSpanExporter : public SpanExporter bool Shutdown(std::chrono::microseconds) noexcept override { return true; } private: - std::vector>* spans_; + std::vector> *spans_; }; /** @@ -62,23 +61,23 @@ 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}; - + 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; } @@ -108,24 +107,24 @@ 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 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()); @@ -136,29 +135,29 @@ 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 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_sampled = false; bool has_unsampled = false; for (const auto &span : spans_) { @@ -177,10 +176,11 @@ TEST_F(BatchSpanProcessorUnsampledTest, EnabledFlagExportsUnsampledSpans) TEST_F(BatchSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) { - // Default options - should not export valid unsampled spans but should export invalid context spans + // 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); @@ -191,13 +191,13 @@ TEST_F(BatchSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) // 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()); } @@ -218,16 +218,16 @@ TEST_F(SimpleSpanProcessorUnsampledTest, DefaultBehaviorDropsUnsampledSpans) std::unique_ptr(new MockSpanExporter(&spans_))); // Create a sampled span and an unsampled span with valid contexts - auto sampled_span = CreateTestSpan(true, true); + 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()); @@ -238,26 +238,26 @@ 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 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_sampled = false; bool has_unsampled = false; for (const auto &span : spans_) { @@ -276,10 +276,11 @@ TEST_F(SimpleSpanProcessorUnsampledTest, EnabledFlagExportsUnsampledSpans) TEST_F(SimpleSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) { - // Default options - should not export valid unsampled spans but should export invalid context spans + // 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); @@ -290,10 +291,10 @@ TEST_F(SimpleSpanProcessorUnsampledTest, InvalidContextSpansAlwaysExported) // 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()); } From 69a0f1c54182373671e8421494ff560b261bc3cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:54:00 +0000 Subject: [PATCH 06/10] Add unsampled_spans_demo to CMake build system Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- examples/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b0991c0c8e..6a193b6628 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,6 +28,10 @@ add_subdirectory(metrics_simple) add_subdirectory(multithreaded) add_subdirectory(multi_processor) +# Add unsampled spans demo example +add_executable(unsampled_spans_demo unsampled_spans_demo.cc) +target_link_libraries(unsampled_spans_demo PRIVATE opentelemetry-cpp::trace) + if(WITH_EXAMPLES_HTTP) add_subdirectory(http) endif() From 4ed9a103698389a186af57856fbfdad61b42b346 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:32:52 +0000 Subject: [PATCH 07/10] Remove export_unsampled_spans feature to ensure OpenTelemetry specification compliance Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- docs/export-unsampled-spans.md | 64 ---- examples/CMakeLists.txt | 4 - examples/unsampled_spans_demo.cc | 137 -------- .../sdk/trace/batch_span_processor.h | 1 - .../sdk/trace/batch_span_processor_options.h | 7 - .../sdk/trace/simple_processor.h | 31 +- .../sdk/trace/simple_processor_options.h | 30 -- sdk/src/trace/batch_span_processor.cc | 19 -- sdk/test/trace/CMakeLists.txt | 3 +- .../trace/unsampled_span_processor_test.cc | 302 ------------------ 10 files changed, 2 insertions(+), 596 deletions(-) delete mode 100644 docs/export-unsampled-spans.md delete mode 100644 examples/unsampled_spans_demo.cc delete mode 100644 sdk/include/opentelemetry/sdk/trace/simple_processor_options.h delete mode 100644 sdk/test/trace/unsampled_span_processor_test.cc diff --git a/docs/export-unsampled-spans.md b/docs/export-unsampled-spans.md deleted file mode 100644 index 81cd38e7ab..0000000000 --- a/docs/export-unsampled-spans.md +++ /dev/null @@ -1,64 +0,0 @@ -# Export Unsampled Spans Feature - -The OpenTelemetry C++ SDK supports an opt-in feature to export unsampled but recording spans from trace processors. This allows collectors to see the full request volume, which is useful for calculating accurate metrics and performing tail-based sampling. - -## Overview - -By default, the C++ SDK only exports sampled spans (`Decision::RECORD_AND_SAMPLE`). With this feature enabled, spans with `Decision::RECORD_ONLY` (unsampled but recording) will also be exported. - -## Usage - -### BatchSpanProcessor - -```cpp -#include "opentelemetry/sdk/trace/batch_span_processor.h" -#include "opentelemetry/sdk/trace/batch_span_processor_options.h" - -// Configure BatchSpanProcessor to export unsampled spans -opentelemetry::sdk::trace::BatchSpanProcessorOptions options; -options.export_unsampled_spans = true; // Default is false - -auto processor = std::make_unique( - std::move(exporter), options); -``` - -### SimpleSpanProcessor - -```cpp -#include "opentelemetry/sdk/trace/simple_processor.h" -#include "opentelemetry/sdk/trace/simple_processor_options.h" - -// Configure SimpleSpanProcessor to export unsampled spans -opentelemetry::sdk::trace::SimpleSpanProcessorOptions options; -options.export_unsampled_spans = true; // Default is false - -auto processor = std::make_unique( - std::move(exporter), options); -``` - -## Behavior - -When `export_unsampled_spans` is: - -- **false** (default): Only sampled spans are exported -- **true**: Both sampled and unsampled recording spans are exported - -## Sampling Decisions - -The feature respects the sampling decisions made during span creation: - -- `Decision::DROP`: Spans are not recorded and never reach the processor (not affected) -- `Decision::RECORD_ONLY`: Spans are recorded but not sampled by default (affected by this feature) -- `Decision::RECORD_AND_SAMPLE`: Spans are recorded and sampled (always exported) - -## Backward Compatibility - -This feature maintains full backward compatibility: - -- Default behavior is unchanged (only sampled spans are exported) -- Existing constructors continue to work as before -- Test spans with invalid contexts are always exported for compatibility - -## Example - -See [examples/unsampled_spans_demo.cc](../examples/unsampled_spans_demo.cc) for a complete working example demonstrating the feature. \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a193b6628..b0991c0c8e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,10 +28,6 @@ add_subdirectory(metrics_simple) add_subdirectory(multithreaded) add_subdirectory(multi_processor) -# Add unsampled spans demo example -add_executable(unsampled_spans_demo unsampled_spans_demo.cc) -target_link_libraries(unsampled_spans_demo PRIVATE opentelemetry-cpp::trace) - if(WITH_EXAMPLES_HTTP) add_subdirectory(http) endif() diff --git a/examples/unsampled_spans_demo.cc b/examples/unsampled_spans_demo.cc deleted file mode 100644 index 5c0579ef74..0000000000 --- a/examples/unsampled_spans_demo.cc +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Example demonstrating export_unsampled_spans feature -// This example shows how to configure span processors to export unsampled spans - -#include -#include - -#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/trace_flags.h" - -namespace trace_sdk = opentelemetry::sdk::trace; -namespace trace_api = opentelemetry::trace; - -// Simple mock exporter for demonstration -class MockExporter : public trace_sdk::SpanExporter -{ -public: - explicit MockExporter(const std::string &name) : name_(name) {} - - std::unique_ptr MakeRecordable() noexcept override - { - return std::make_unique(); - } - - opentelemetry::sdk::common::ExportResult Export( - const opentelemetry::nostd::span> - &recordables) noexcept override - { - std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl; - 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::string name_; -}; - -// Create a test span with specific sampling status -std::unique_ptr CreateTestSpan(bool sampled) -{ - auto span = std::make_unique(); - span->SetName("test_span"); - - // Set up valid context with proper sampling - trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0); - 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}; - - trace_api::TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; - trace_api::SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; - - trace_api::SpanContext context(trace_id, span_id, flags, false); - span->SetIdentity(context, trace_api::SpanId()); - - return span; -} - -int main() -{ - std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl; - - // Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior) - { - std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl; - - trace_sdk::BatchSpanProcessorOptions options; - // export_unsampled_spans is false by default - - auto processor = std::make_unique( - std::make_unique("BatchProcessor-Default"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - processor->ForceFlush(); - std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl; - } - - // Example 2: BatchSpanProcessor with export_unsampled_spans enabled - { - std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" - << std::endl; - - trace_sdk::BatchSpanProcessorOptions options; - options.export_unsampled_spans = true; // Enable exporting unsampled spans - - auto processor = std::make_unique( - std::make_unique("BatchProcessor-WithUnsampled"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - processor->ForceFlush(); - std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl; - } - - // Example 3: SimpleSpanProcessor with export_unsampled_spans enabled - { - std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" - << std::endl; - - trace_sdk::SimpleSpanProcessorOptions options; - options.export_unsampled_spans = true; // Enable exporting unsampled spans - - auto processor = std::make_unique( - std::make_unique("SimpleProcessor-WithUnsampled"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl; - } - - std::cout << "Demo completed!" << std::endl; - return 0; -} \ No newline at end of file diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h index d184fff4d6..3a78d6f33c 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h @@ -164,7 +164,6 @@ 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 3f73677f67..ac1052383b 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h @@ -33,13 +33,6 @@ 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. - * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported. - */ - 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 3332296d78..ce6d378b79 100644 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor.h @@ -15,8 +15,6 @@ #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" @@ -42,17 +40,7 @@ class SimpleSpanProcessor : public SpanProcessor * @param exporter the exporter used by the span processor */ explicit SimpleSpanProcessor(std::unique_ptr &&exporter) noexcept - : 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) + : exporter_(std::move(exporter)) {} std::unique_ptr MakeRecordable() noexcept override @@ -66,22 +54,6 @@ 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) @@ -117,7 +89,6 @@ 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 deleted file mode 100644 index 0886434f06..0000000000 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h +++ /dev/null @@ -1,30 +0,0 @@ -// 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. - * When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported. - */ - bool export_unsampled_spans = false; -}; - -} // namespace trace -} // namespace sdk -OPENTELEMETRY_END_NAMESPACE \ No newline at end of file diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index 078ac869cc..5ec2376656 100644 --- a/sdk/src/trace/batch_span_processor.cc +++ b/sdk/src/trace/batch_span_processor.cc @@ -25,7 +25,6 @@ #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 @@ -48,7 +47,6 @@ 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), @@ -65,7 +63,6 @@ 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), @@ -92,22 +89,6 @@ 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 9a560fd6e0..de8dee2340 100644 --- a/sdk/test/trace/CMakeLists.txt +++ b/sdk/test/trace/CMakeLists.txt @@ -13,8 +13,7 @@ foreach( parent_sampler_test trace_id_ratio_sampler_test batch_span_processor_test - tracer_config_test - unsampled_span_processor_test) + tracer_config_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 deleted file mode 100644 index 8313a927cf..0000000000 --- a/sdk/test/trace/unsampled_span_processor_test.cc +++ /dev/null @@ -1,302 +0,0 @@ -// 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 From 037e44d6c83bf87f8a17052c8d6b934a41cf3e21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 17:44:27 +0000 Subject: [PATCH 08/10] Restore export_unsampled_spans as opt-in feature maintaining spec compliance by default Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- docs/export-unsampled-spans.md | 66 ++++ examples/CMakeLists.txt | 4 + examples/unsampled_spans_demo.cc | 137 ++++++++ .../sdk/trace/batch_span_processor.h | 1 + .../sdk/trace/batch_span_processor_options.h | 9 + .../sdk/trace/simple_processor.h | 31 +- .../sdk/trace/simple_processor_options.h | 32 ++ sdk/src/trace/batch_span_processor.cc | 19 ++ sdk/test/trace/CMakeLists.txt | 3 +- .../trace/unsampled_span_processor_test.cc | 302 ++++++++++++++++++ 10 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 docs/export-unsampled-spans.md create mode 100644 examples/unsampled_spans_demo.cc create mode 100644 sdk/include/opentelemetry/sdk/trace/simple_processor_options.h create mode 100644 sdk/test/trace/unsampled_span_processor_test.cc diff --git a/docs/export-unsampled-spans.md b/docs/export-unsampled-spans.md new file mode 100644 index 0000000000..03edf4d455 --- /dev/null +++ b/docs/export-unsampled-spans.md @@ -0,0 +1,66 @@ +# Export Unsampled Spans Feature + +The OpenTelemetry C++ SDK supports an opt-in feature to export unsampled but recording spans from trace processors. This allows collectors to see the full request volume, which is useful for calculating accurate metrics and performing tail-based sampling. + +**⚠️ IMPORTANT**: This feature intentionally violates the OpenTelemetry specification when enabled. According to the OpenTelemetry specification, RECORD_ONLY spans should be processed by SpanProcessors but should NEVER be sent to SpanExporters. Use this feature only when you specifically need this behavior and understand the specification implications. + +## Overview + +By default, the C++ SDK maintains OpenTelemetry specification compliance by only exporting sampled spans (`Decision::RECORD_AND_SAMPLE`). With this feature enabled, spans with `Decision::RECORD_ONLY` (unsampled but recording) will also be exported, which violates the specification. + +## Usage + +### BatchSpanProcessor + +```cpp +#include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" + +// Configure BatchSpanProcessor to export unsampled spans +opentelemetry::sdk::trace::BatchSpanProcessorOptions options; +options.export_unsampled_spans = true; // Default is false + +auto processor = std::make_unique( + std::move(exporter), options); +``` + +### SimpleSpanProcessor + +```cpp +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/simple_processor_options.h" + +// Configure SimpleSpanProcessor to export unsampled spans +opentelemetry::sdk::trace::SimpleSpanProcessorOptions options; +options.export_unsampled_spans = true; // Default is false + +auto processor = std::make_unique( + std::move(exporter), options); +``` + +## Behavior + +When `export_unsampled_spans` is: + +- **false** (default): Only sampled spans are exported +- **true**: Both sampled and unsampled recording spans are exported + +## Sampling Decisions + +The feature respects the sampling decisions made during span creation: + +- `Decision::DROP`: Spans are not recorded and never reach the processor (not affected) +- `Decision::RECORD_ONLY`: Spans are recorded but not sampled by default (affected by this feature) +- `Decision::RECORD_AND_SAMPLE`: Spans are recorded and sampled (always exported) + +## Backward Compatibility + +This feature maintains full backward compatibility: + +- Default behavior is unchanged (only sampled spans are exported) +- Existing constructors continue to work as before +- Test spans with invalid contexts are always exported for compatibility + +## Example + +See [examples/unsampled_spans_demo.cc](../examples/unsampled_spans_demo.cc) for a complete working example demonstrating the feature. \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b0991c0c8e..6a193b6628 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,6 +28,10 @@ add_subdirectory(metrics_simple) add_subdirectory(multithreaded) add_subdirectory(multi_processor) +# Add unsampled spans demo example +add_executable(unsampled_spans_demo unsampled_spans_demo.cc) +target_link_libraries(unsampled_spans_demo PRIVATE opentelemetry-cpp::trace) + if(WITH_EXAMPLES_HTTP) add_subdirectory(http) endif() diff --git a/examples/unsampled_spans_demo.cc b/examples/unsampled_spans_demo.cc new file mode 100644 index 0000000000..5c0579ef74 --- /dev/null +++ b/examples/unsampled_spans_demo.cc @@ -0,0 +1,137 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Example demonstrating export_unsampled_spans feature +// This example shows how to configure span processors to export unsampled spans + +#include +#include + +#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/trace_flags.h" + +namespace trace_sdk = opentelemetry::sdk::trace; +namespace trace_api = opentelemetry::trace; + +// Simple mock exporter for demonstration +class MockExporter : public trace_sdk::SpanExporter +{ +public: + explicit MockExporter(const std::string &name) : name_(name) {} + + std::unique_ptr MakeRecordable() noexcept override + { + return std::make_unique(); + } + + opentelemetry::sdk::common::ExportResult Export( + const opentelemetry::nostd::span> + &recordables) noexcept override + { + std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl; + 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::string name_; +}; + +// Create a test span with specific sampling status +std::unique_ptr CreateTestSpan(bool sampled) +{ + auto span = std::make_unique(); + span->SetName("test_span"); + + // Set up valid context with proper sampling + trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0); + 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}; + + trace_api::TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; + trace_api::SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; + + trace_api::SpanContext context(trace_id, span_id, flags, false); + span->SetIdentity(context, trace_api::SpanId()); + + return span; +} + +int main() +{ + std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl; + + // Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior) + { + std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl; + + trace_sdk::BatchSpanProcessorOptions options; + // export_unsampled_spans is false by default + + auto processor = std::make_unique( + std::make_unique("BatchProcessor-Default"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + processor->ForceFlush(); + std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl; + } + + // Example 2: BatchSpanProcessor with export_unsampled_spans enabled + { + std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" + << std::endl; + + trace_sdk::BatchSpanProcessorOptions options; + options.export_unsampled_spans = true; // Enable exporting unsampled spans + + auto processor = std::make_unique( + std::make_unique("BatchProcessor-WithUnsampled"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + processor->ForceFlush(); + std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl; + } + + // Example 3: SimpleSpanProcessor with export_unsampled_spans enabled + { + std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" + << std::endl; + + trace_sdk::SimpleSpanProcessorOptions options; + options.export_unsampled_spans = true; // Enable exporting unsampled spans + + auto processor = std::make_unique( + std::make_unique("SimpleProcessor-WithUnsampled"), options); + + // Create one sampled and one unsampled span + auto sampled_span = CreateTestSpan(true); + auto unsampled_span = CreateTestSpan(false); + + processor->OnEnd(std::unique_ptr(sampled_span.release())); + processor->OnEnd(std::unique_ptr(unsampled_span.release())); + + std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl; + } + + std::cout << "Demo completed!" << std::endl; + return 0; +} \ No newline at end of file diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h index 3a78d6f33c..d184fff4d6 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h @@ -164,6 +164,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 ce6d378b79..3332296d78 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) @@ -89,6 +117,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..34657208f8 --- /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 \ No newline at end of file diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index 5ec2376656..078ac869cc 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 From a5ce117b4303d094e027f44b59fc8d50bebd4b77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:40:28 +0000 Subject: [PATCH 09/10] Remove markdown documentation and example for export_unsampled_spans feature Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- docs/export-unsampled-spans.md | 66 --------------- examples/CMakeLists.txt | 4 - examples/unsampled_spans_demo.cc | 137 ------------------------------- 3 files changed, 207 deletions(-) delete mode 100644 docs/export-unsampled-spans.md delete mode 100644 examples/unsampled_spans_demo.cc diff --git a/docs/export-unsampled-spans.md b/docs/export-unsampled-spans.md deleted file mode 100644 index 03edf4d455..0000000000 --- a/docs/export-unsampled-spans.md +++ /dev/null @@ -1,66 +0,0 @@ -# Export Unsampled Spans Feature - -The OpenTelemetry C++ SDK supports an opt-in feature to export unsampled but recording spans from trace processors. This allows collectors to see the full request volume, which is useful for calculating accurate metrics and performing tail-based sampling. - -**⚠️ IMPORTANT**: This feature intentionally violates the OpenTelemetry specification when enabled. According to the OpenTelemetry specification, RECORD_ONLY spans should be processed by SpanProcessors but should NEVER be sent to SpanExporters. Use this feature only when you specifically need this behavior and understand the specification implications. - -## Overview - -By default, the C++ SDK maintains OpenTelemetry specification compliance by only exporting sampled spans (`Decision::RECORD_AND_SAMPLE`). With this feature enabled, spans with `Decision::RECORD_ONLY` (unsampled but recording) will also be exported, which violates the specification. - -## Usage - -### BatchSpanProcessor - -```cpp -#include "opentelemetry/sdk/trace/batch_span_processor.h" -#include "opentelemetry/sdk/trace/batch_span_processor_options.h" - -// Configure BatchSpanProcessor to export unsampled spans -opentelemetry::sdk::trace::BatchSpanProcessorOptions options; -options.export_unsampled_spans = true; // Default is false - -auto processor = std::make_unique( - std::move(exporter), options); -``` - -### SimpleSpanProcessor - -```cpp -#include "opentelemetry/sdk/trace/simple_processor.h" -#include "opentelemetry/sdk/trace/simple_processor_options.h" - -// Configure SimpleSpanProcessor to export unsampled spans -opentelemetry::sdk::trace::SimpleSpanProcessorOptions options; -options.export_unsampled_spans = true; // Default is false - -auto processor = std::make_unique( - std::move(exporter), options); -``` - -## Behavior - -When `export_unsampled_spans` is: - -- **false** (default): Only sampled spans are exported -- **true**: Both sampled and unsampled recording spans are exported - -## Sampling Decisions - -The feature respects the sampling decisions made during span creation: - -- `Decision::DROP`: Spans are not recorded and never reach the processor (not affected) -- `Decision::RECORD_ONLY`: Spans are recorded but not sampled by default (affected by this feature) -- `Decision::RECORD_AND_SAMPLE`: Spans are recorded and sampled (always exported) - -## Backward Compatibility - -This feature maintains full backward compatibility: - -- Default behavior is unchanged (only sampled spans are exported) -- Existing constructors continue to work as before -- Test spans with invalid contexts are always exported for compatibility - -## Example - -See [examples/unsampled_spans_demo.cc](../examples/unsampled_spans_demo.cc) for a complete working example demonstrating the feature. \ No newline at end of file diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6a193b6628..b0991c0c8e 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,10 +28,6 @@ add_subdirectory(metrics_simple) add_subdirectory(multithreaded) add_subdirectory(multi_processor) -# Add unsampled spans demo example -add_executable(unsampled_spans_demo unsampled_spans_demo.cc) -target_link_libraries(unsampled_spans_demo PRIVATE opentelemetry-cpp::trace) - if(WITH_EXAMPLES_HTTP) add_subdirectory(http) endif() diff --git a/examples/unsampled_spans_demo.cc b/examples/unsampled_spans_demo.cc deleted file mode 100644 index 5c0579ef74..0000000000 --- a/examples/unsampled_spans_demo.cc +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// Example demonstrating export_unsampled_spans feature -// This example shows how to configure span processors to export unsampled spans - -#include -#include - -#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/trace_flags.h" - -namespace trace_sdk = opentelemetry::sdk::trace; -namespace trace_api = opentelemetry::trace; - -// Simple mock exporter for demonstration -class MockExporter : public trace_sdk::SpanExporter -{ -public: - explicit MockExporter(const std::string &name) : name_(name) {} - - std::unique_ptr MakeRecordable() noexcept override - { - return std::make_unique(); - } - - opentelemetry::sdk::common::ExportResult Export( - const opentelemetry::nostd::span> - &recordables) noexcept override - { - std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl; - 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::string name_; -}; - -// Create a test span with specific sampling status -std::unique_ptr CreateTestSpan(bool sampled) -{ - auto span = std::make_unique(); - span->SetName("test_span"); - - // Set up valid context with proper sampling - trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0); - 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}; - - trace_api::TraceId trace_id{opentelemetry::nostd::span(trace_id_bytes)}; - trace_api::SpanId span_id{opentelemetry::nostd::span(span_id_bytes)}; - - trace_api::SpanContext context(trace_id, span_id, flags, false); - span->SetIdentity(context, trace_api::SpanId()); - - return span; -} - -int main() -{ - std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl; - - // Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior) - { - std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl; - - trace_sdk::BatchSpanProcessorOptions options; - // export_unsampled_spans is false by default - - auto processor = std::make_unique( - std::make_unique("BatchProcessor-Default"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - processor->ForceFlush(); - std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl; - } - - // Example 2: BatchSpanProcessor with export_unsampled_spans enabled - { - std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ===" - << std::endl; - - trace_sdk::BatchSpanProcessorOptions options; - options.export_unsampled_spans = true; // Enable exporting unsampled spans - - auto processor = std::make_unique( - std::make_unique("BatchProcessor-WithUnsampled"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - processor->ForceFlush(); - std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl; - } - - // Example 3: SimpleSpanProcessor with export_unsampled_spans enabled - { - std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ===" - << std::endl; - - trace_sdk::SimpleSpanProcessorOptions options; - options.export_unsampled_spans = true; // Enable exporting unsampled spans - - auto processor = std::make_unique( - std::make_unique("SimpleProcessor-WithUnsampled"), options); - - // Create one sampled and one unsampled span - auto sampled_span = CreateTestSpan(true); - auto unsampled_span = CreateTestSpan(false); - - processor->OnEnd(std::unique_ptr(sampled_span.release())); - processor->OnEnd(std::unique_ptr(unsampled_span.release())); - - std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl; - } - - std::cout << "Demo completed!" << std::endl; - return 0; -} \ No newline at end of file From a0dbb94db960af9521c62c821fca7d2686521c51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:49:33 +0000 Subject: [PATCH 10/10] Fix missing newline at end of simple_processor_options.h file Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com> --- sdk/include/opentelemetry/sdk/trace/simple_processor_options.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h b/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h index 34657208f8..6ee911e908 100644 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor_options.h @@ -29,4 +29,4 @@ struct SimpleSpanProcessorOptions } // namespace trace } // namespace sdk -OPENTELEMETRY_END_NAMESPACE \ No newline at end of file +OPENTELEMETRY_END_NAMESPACE