Skip to content

Commit 037e44d

Browse files
Copilotlalitb
andcommitted
Restore export_unsampled_spans as opt-in feature maintaining spec compliance by default
Co-authored-by: lalitb <1196320+lalitb@users.noreply.github.com>
1 parent 4ed9a10 commit 037e44d

File tree

10 files changed

+602
-2
lines changed

10 files changed

+602
-2
lines changed

docs/export-unsampled-spans.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Export Unsampled Spans Feature
2+
3+
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.
4+
5+
**⚠️ 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.
6+
7+
## Overview
8+
9+
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.
10+
11+
## Usage
12+
13+
### BatchSpanProcessor
14+
15+
```cpp
16+
#include "opentelemetry/sdk/trace/batch_span_processor.h"
17+
#include "opentelemetry/sdk/trace/batch_span_processor_options.h"
18+
19+
// Configure BatchSpanProcessor to export unsampled spans
20+
opentelemetry::sdk::trace::BatchSpanProcessorOptions options;
21+
options.export_unsampled_spans = true; // Default is false
22+
23+
auto processor = std::make_unique<opentelemetry::sdk::trace::BatchSpanProcessor>(
24+
std::move(exporter), options);
25+
```
26+
27+
### SimpleSpanProcessor
28+
29+
```cpp
30+
#include "opentelemetry/sdk/trace/simple_processor.h"
31+
#include "opentelemetry/sdk/trace/simple_processor_options.h"
32+
33+
// Configure SimpleSpanProcessor to export unsampled spans
34+
opentelemetry::sdk::trace::SimpleSpanProcessorOptions options;
35+
options.export_unsampled_spans = true; // Default is false
36+
37+
auto processor = std::make_unique<opentelemetry::sdk::trace::SimpleSpanProcessor>(
38+
std::move(exporter), options);
39+
```
40+
41+
## Behavior
42+
43+
When `export_unsampled_spans` is:
44+
45+
- **false** (default): Only sampled spans are exported
46+
- **true**: Both sampled and unsampled recording spans are exported
47+
48+
## Sampling Decisions
49+
50+
The feature respects the sampling decisions made during span creation:
51+
52+
- `Decision::DROP`: Spans are not recorded and never reach the processor (not affected)
53+
- `Decision::RECORD_ONLY`: Spans are recorded but not sampled by default (affected by this feature)
54+
- `Decision::RECORD_AND_SAMPLE`: Spans are recorded and sampled (always exported)
55+
56+
## Backward Compatibility
57+
58+
This feature maintains full backward compatibility:
59+
60+
- Default behavior is unchanged (only sampled spans are exported)
61+
- Existing constructors continue to work as before
62+
- Test spans with invalid contexts are always exported for compatibility
63+
64+
## Example
65+
66+
See [examples/unsampled_spans_demo.cc](../examples/unsampled_spans_demo.cc) for a complete working example demonstrating the feature.

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ add_subdirectory(metrics_simple)
2828
add_subdirectory(multithreaded)
2929
add_subdirectory(multi_processor)
3030

31+
# Add unsampled spans demo example
32+
add_executable(unsampled_spans_demo unsampled_spans_demo.cc)
33+
target_link_libraries(unsampled_spans_demo PRIVATE opentelemetry-cpp::trace)
34+
3135
if(WITH_EXAMPLES_HTTP)
3236
add_subdirectory(http)
3337
endif()

examples/unsampled_spans_demo.cc

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Example demonstrating export_unsampled_spans feature
5+
// This example shows how to configure span processors to export unsampled spans
6+
7+
#include <iostream>
8+
#include <memory>
9+
10+
#include "opentelemetry/sdk/trace/batch_span_processor.h"
11+
#include "opentelemetry/sdk/trace/batch_span_processor_options.h"
12+
#include "opentelemetry/sdk/trace/simple_processor.h"
13+
#include "opentelemetry/sdk/trace/simple_processor_options.h"
14+
#include "opentelemetry/sdk/trace/span_data.h"
15+
#include "opentelemetry/trace/span_context.h"
16+
#include "opentelemetry/trace/trace_flags.h"
17+
18+
namespace trace_sdk = opentelemetry::sdk::trace;
19+
namespace trace_api = opentelemetry::trace;
20+
21+
// Simple mock exporter for demonstration
22+
class MockExporter : public trace_sdk::SpanExporter
23+
{
24+
public:
25+
explicit MockExporter(const std::string &name) : name_(name) {}
26+
27+
std::unique_ptr<trace_sdk::Recordable> MakeRecordable() noexcept override
28+
{
29+
return std::make_unique<trace_sdk::SpanData>();
30+
}
31+
32+
opentelemetry::sdk::common::ExportResult Export(
33+
const opentelemetry::nostd::span<std::unique_ptr<trace_sdk::Recordable>>
34+
&recordables) noexcept override
35+
{
36+
std::cout << name_ << " exported " << recordables.size() << " spans" << std::endl;
37+
return opentelemetry::sdk::common::ExportResult::kSuccess;
38+
}
39+
40+
bool ForceFlush(std::chrono::microseconds) noexcept override { return true; }
41+
bool Shutdown(std::chrono::microseconds) noexcept override { return true; }
42+
43+
private:
44+
std::string name_;
45+
};
46+
47+
// Create a test span with specific sampling status
48+
std::unique_ptr<trace_sdk::SpanData> CreateTestSpan(bool sampled)
49+
{
50+
auto span = std::make_unique<trace_sdk::SpanData>();
51+
span->SetName("test_span");
52+
53+
// Set up valid context with proper sampling
54+
trace_api::TraceFlags flags(sampled ? trace_api::TraceFlags::kIsSampled : 0);
55+
uint8_t trace_id_bytes[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
56+
uint8_t span_id_bytes[8] = {1, 2, 3, 4, 5, 6, 7, 8};
57+
58+
trace_api::TraceId trace_id{opentelemetry::nostd::span<const uint8_t, 16>(trace_id_bytes)};
59+
trace_api::SpanId span_id{opentelemetry::nostd::span<const uint8_t, 8>(span_id_bytes)};
60+
61+
trace_api::SpanContext context(trace_id, span_id, flags, false);
62+
span->SetIdentity(context, trace_api::SpanId());
63+
64+
return span;
65+
}
66+
67+
int main()
68+
{
69+
std::cout << "OpenTelemetry C++ Export Unsampled Spans Demo\n" << std::endl;
70+
71+
// Example 1: BatchSpanProcessor without export_unsampled_spans (default behavior)
72+
{
73+
std::cout << "=== Example 1: BatchSpanProcessor (default behavior) ===" << std::endl;
74+
75+
trace_sdk::BatchSpanProcessorOptions options;
76+
// export_unsampled_spans is false by default
77+
78+
auto processor = std::make_unique<trace_sdk::BatchSpanProcessor>(
79+
std::make_unique<MockExporter>("BatchProcessor-Default"), options);
80+
81+
// Create one sampled and one unsampled span
82+
auto sampled_span = CreateTestSpan(true);
83+
auto unsampled_span = CreateTestSpan(false);
84+
85+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(sampled_span.release()));
86+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(unsampled_span.release()));
87+
88+
processor->ForceFlush();
89+
std::cout << "Expected: Only 1 span exported (sampled span only)\n" << std::endl;
90+
}
91+
92+
// Example 2: BatchSpanProcessor with export_unsampled_spans enabled
93+
{
94+
std::cout << "=== Example 2: BatchSpanProcessor (export_unsampled_spans = true) ==="
95+
<< std::endl;
96+
97+
trace_sdk::BatchSpanProcessorOptions options;
98+
options.export_unsampled_spans = true; // Enable exporting unsampled spans
99+
100+
auto processor = std::make_unique<trace_sdk::BatchSpanProcessor>(
101+
std::make_unique<MockExporter>("BatchProcessor-WithUnsampled"), options);
102+
103+
// Create one sampled and one unsampled span
104+
auto sampled_span = CreateTestSpan(true);
105+
auto unsampled_span = CreateTestSpan(false);
106+
107+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(sampled_span.release()));
108+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(unsampled_span.release()));
109+
110+
processor->ForceFlush();
111+
std::cout << "Expected: 2 spans exported (both sampled and unsampled)\n" << std::endl;
112+
}
113+
114+
// Example 3: SimpleSpanProcessor with export_unsampled_spans enabled
115+
{
116+
std::cout << "=== Example 3: SimpleSpanProcessor (export_unsampled_spans = true) ==="
117+
<< std::endl;
118+
119+
trace_sdk::SimpleSpanProcessorOptions options;
120+
options.export_unsampled_spans = true; // Enable exporting unsampled spans
121+
122+
auto processor = std::make_unique<trace_sdk::SimpleSpanProcessor>(
123+
std::make_unique<MockExporter>("SimpleProcessor-WithUnsampled"), options);
124+
125+
// Create one sampled and one unsampled span
126+
auto sampled_span = CreateTestSpan(true);
127+
auto unsampled_span = CreateTestSpan(false);
128+
129+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(sampled_span.release()));
130+
processor->OnEnd(std::unique_ptr<trace_sdk::Recordable>(unsampled_span.release()));
131+
132+
std::cout << "Expected: 2 separate exports (one for each span)\n" << std::endl;
133+
}
134+
135+
std::cout << "Demo completed!" << std::endl;
136+
return 0;
137+
}

sdk/include/opentelemetry/sdk/trace/batch_span_processor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class BatchSpanProcessor : public SpanProcessor
164164
const size_t max_queue_size_;
165165
const std::chrono::milliseconds schedule_delay_millis_;
166166
const size_t max_export_batch_size_;
167+
const bool export_unsampled_spans_;
167168

168169
/* The buffer/queue to which the ended spans are added */
169170
opentelemetry::sdk::common::CircularBuffer<Recordable> buffer_;

sdk/include/opentelemetry/sdk/trace/batch_span_processor_options.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ struct BatchSpanProcessorOptions
3333
* equal to max_queue_size.
3434
*/
3535
size_t max_export_batch_size = 512;
36+
37+
/**
38+
* Whether to export unsampled but recording spans.
39+
* By default, only sampled spans (Decision::RECORD_AND_SAMPLE) are exported to maintain
40+
* OpenTelemetry specification compliance.
41+
* When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported,
42+
* which intentionally violates the OpenTelemetry specification.
43+
*/
44+
bool export_unsampled_spans = false;
3645
};
3746

3847
} // namespace trace

sdk/include/opentelemetry/sdk/trace/simple_processor.h

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include "opentelemetry/sdk/trace/exporter.h"
1616
#include "opentelemetry/sdk/trace/processor.h"
1717
#include "opentelemetry/sdk/trace/recordable.h"
18+
#include "opentelemetry/sdk/trace/simple_processor_options.h"
19+
#include "opentelemetry/sdk/trace/span_data.h"
1820
#include "opentelemetry/trace/span_context.h"
1921
#include "opentelemetry/version.h"
2022

@@ -40,7 +42,17 @@ class SimpleSpanProcessor : public SpanProcessor
4042
* @param exporter the exporter used by the span processor
4143
*/
4244
explicit SimpleSpanProcessor(std::unique_ptr<SpanExporter> &&exporter) noexcept
43-
: exporter_(std::move(exporter))
45+
: exporter_(std::move(exporter)), export_unsampled_spans_(false)
46+
{}
47+
48+
/**
49+
* Initialize a simple span processor with options.
50+
* @param exporter the exporter used by the span processor
51+
* @param options the processor options
52+
*/
53+
explicit SimpleSpanProcessor(std::unique_ptr<SpanExporter> &&exporter,
54+
const SimpleSpanProcessorOptions &options) noexcept
55+
: exporter_(std::move(exporter)), export_unsampled_spans_(options.export_unsampled_spans)
4456
{}
4557

4658
std::unique_ptr<Recordable> MakeRecordable() noexcept override
@@ -54,6 +66,22 @@ class SimpleSpanProcessor : public SpanProcessor
5466

5567
void OnEnd(std::unique_ptr<Recordable> &&span) noexcept override
5668
{
69+
// Check if we should export this span based on sampling status
70+
auto *span_data = static_cast<SpanData *>(span.get());
71+
const auto &span_context = span_data->GetSpanContext();
72+
73+
// For backward compatibility: always export spans with invalid context (e.g., test spans)
74+
// For valid contexts: export sampled spans or unsampled spans if export_unsampled_spans is
75+
// enabled
76+
bool should_export =
77+
!span_context.IsValid() || span_context.IsSampled() || export_unsampled_spans_;
78+
79+
if (!should_export)
80+
{
81+
// Drop unsampled spans if export_unsampled_spans is not enabled
82+
return;
83+
}
84+
5785
nostd::span<std::unique_ptr<Recordable>> batch(&span, 1);
5886
const std::lock_guard<opentelemetry::common::SpinLockMutex> locked(lock_);
5987
if (exporter_->Export(batch) == sdk::common::ExportResult::kFailure)
@@ -89,6 +117,7 @@ class SimpleSpanProcessor : public SpanProcessor
89117

90118
private:
91119
std::unique_ptr<SpanExporter> exporter_;
120+
const bool export_unsampled_spans_;
92121
opentelemetry::common::SpinLockMutex lock_;
93122
#if defined(__cpp_lib_atomic_value_initialization) && \
94123
__cpp_lib_atomic_value_initialization >= 201911L
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#pragma once
5+
6+
#include "opentelemetry/version.h"
7+
8+
OPENTELEMETRY_BEGIN_NAMESPACE
9+
namespace sdk
10+
{
11+
12+
namespace trace
13+
{
14+
15+
/**
16+
* Struct to hold simple SpanProcessor options.
17+
*/
18+
struct SimpleSpanProcessorOptions
19+
{
20+
/**
21+
* Whether to export unsampled but recording spans.
22+
* By default, only sampled spans (Decision::RECORD_AND_SAMPLE) are exported to maintain
23+
* OpenTelemetry specification compliance.
24+
* When set to true, unsampled recording spans (Decision::RECORD_ONLY) are also exported,
25+
* which intentionally violates the OpenTelemetry specification.
26+
*/
27+
bool export_unsampled_spans = false;
28+
};
29+
30+
} // namespace trace
31+
} // namespace sdk
32+
OPENTELEMETRY_END_NAMESPACE

sdk/src/trace/batch_span_processor.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "opentelemetry/sdk/trace/exporter.h"
2626
#include "opentelemetry/sdk/trace/processor.h"
2727
#include "opentelemetry/sdk/trace/recordable.h"
28+
#include "opentelemetry/sdk/trace/span_data.h"
2829
#include "opentelemetry/version.h"
2930

3031
#ifdef ENABLE_THREAD_INSTRUMENTATION_PREVIEW
@@ -47,6 +48,7 @@ BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr<SpanExporter> &&exporter,
4748
max_queue_size_(options.max_queue_size),
4849
schedule_delay_millis_(options.schedule_delay_millis),
4950
max_export_batch_size_(options.max_export_batch_size),
51+
export_unsampled_spans_(options.export_unsampled_spans),
5052
buffer_(max_queue_size_),
5153
synchronization_data_(std::make_shared<SynchronizationData>()),
5254
worker_thread_instrumentation_(nullptr),
@@ -63,6 +65,7 @@ BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr<SpanExporter> &&exporter,
6365
max_queue_size_(options.max_queue_size),
6466
schedule_delay_millis_(options.schedule_delay_millis),
6567
max_export_batch_size_(options.max_export_batch_size),
68+
export_unsampled_spans_(options.export_unsampled_spans),
6669
buffer_(max_queue_size_),
6770
synchronization_data_(std::make_shared<SynchronizationData>()),
6871
worker_thread_instrumentation_(runtime_options.thread_instrumentation),
@@ -89,6 +92,22 @@ void BatchSpanProcessor::OnEnd(std::unique_ptr<Recordable> &&span) noexcept
8992
return;
9093
}
9194

95+
// Check if we should export this span based on sampling status
96+
auto *span_data = static_cast<SpanData *>(span.get());
97+
const auto &span_context = span_data->GetSpanContext();
98+
99+
// For backward compatibility: always export spans with invalid context (e.g., test spans)
100+
// For valid contexts: export sampled spans or unsampled spans if export_unsampled_spans is
101+
// enabled
102+
bool should_export =
103+
!span_context.IsValid() || span_context.IsSampled() || export_unsampled_spans_;
104+
105+
if (!should_export)
106+
{
107+
// Drop unsampled spans if export_unsampled_spans is not enabled
108+
return;
109+
}
110+
92111
if (buffer_.Add(std::move(span)) == false)
93112
{
94113
OTEL_INTERNAL_LOG_WARN("BatchSpanProcessor queue is full - dropping span.");

sdk/test/trace/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ foreach(
1313
parent_sampler_test
1414
trace_id_ratio_sampler_test
1515
batch_span_processor_test
16-
tracer_config_test)
16+
tracer_config_test
17+
unsampled_span_processor_test)
1718
add_executable(${testname} "${testname}.cc")
1819
target_link_libraries(
1920
${testname}

0 commit comments

Comments
 (0)