Skip to content

Commit 75c8a39

Browse files
feat: add span flags support for isRemote property (#1917)
* feat(otlp-exporter): add span flags support for isRemote property This commit implements the missing isRemote flag functionality in the Ruby OpenTelemetry SDK's OTLP exporter, bringing it to feature parity with Go, Python, PHP, and JavaScript SDKs. Changes: - Add span flags constants matching OTLP specification (0x100, 0x200) - Implement build_span_flags method following OTLP spec - Extend SpanData to include parent_span_context field - Update span creation to store parent span context - Modify OTLP exporter to set flags for both spans and links - Update protobuf definitions to include flags field - Add comprehensive test coverage for all scenarios The implementation follows the exact same pattern as other OpenTelemetry SDKs, ensuring consistency across language implementations. Flag values: - Local context: 0x100 (SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) - Remote context: 0x300 (HAS_IS_REMOTE_MASK | IS_REMOTE_MASK) Fixes: Missing span flags support in OTLP exporter Resolves: Feature parity with other OpenTelemetry SDKs * chore: updated changelog PR number * chore: added passing of parent_span_is_remote boolean in place of parent_span_context * fix: move isRemote implementation to correct production exporter - Revert changes from exporter/otlp-common (incomplete refactoring) - Remove span_flags.rb and tests from otlp-common - Add span_flags.rb to correct location in exporter/otlp - Implementation now in production exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb This addresses reviewer feedback that the common.rb file was not the production version. * refactor: improve build_span_flags function based on reviewer feedback - Remove default value for base_flags parameter (make it required) - Combine flag initialization into single line for better conciseness - Maintains same functionality while improving code clarity Addresses reviewer suggestion for cleaner, more explicit function signature. * refactor: remove intermediate variable assignment in as_otlp_span - Remove span variable assignment and use .tap method instead - More concise code while maintaining same functionality - Addresses reviewer suggestion for cleaner code structure The .tap method allows us to set the flags field and return the span object in a single expression, eliminating the need for an intermediate variable. * refactor: pass flags directly as named argument to Span.new - Remove .tap method and pass flags: as named argument to Span.new constructor - More concise and cleaner code structure - Addresses reviewer suggestion for direct parameter passing This eliminates the need for separate span.flags assignment and makes the code more readable by setting all span properties in one place. * refactor: pass flags directly as named argument to Link.new - Remove intermediate link_proto variable assignment - Pass flags: as named argument directly to Link.new constructor - Apply same pattern as span creation for consistency - More concise and cleaner code structure This completes the refactoring to pass flags directly in constructors for both spans and links, eliminating separate assignment steps. * chore: implemented changes in expected file and improved code post review * fix: fix for failing test cases * fix: moved constants to a suitable location * failing test changes * chore: delete manual constants for span_flags and use the latest from the protobuf updates for the SDK * fix: added fix for workflow failing tests * fix: failing workflows * fix: replace magic number with SAMPLED constant * fix: all robocop offenses * fix: update base_flags_int and test objects * fix: do not pass integers to build_span_flags * chore: removed changelog --------- Co-authored-by: Kayla Reopelle <[email protected]>
1 parent 90ae798 commit 75c8a39

File tree

4 files changed

+187
-6
lines changed

4 files changed

+187
-6
lines changed

exporter/otlp/lib/opentelemetry/exporter/otlp/exporter.rb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ def shutdown(timeout: nil)
103103

104104
private
105105

106+
# Builds span flags based on whether the parent span context is remote.
107+
# This follows the OTLP specification for span flags.
108+
def build_span_flags(parent_span_is_remote, base_flags)
109+
# Extract integer value from TraceFlags object if needed
110+
# Derive the low 8-bit W3C trace flags using the public API.
111+
base_flags_int =
112+
if base_flags.sampled?
113+
1
114+
else
115+
0
116+
end
117+
118+
has_remote_mask = Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
119+
is_remote_mask = Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
120+
121+
flags = base_flags_int | has_remote_mask
122+
flags |= is_remote_mask if parent_span_is_remote
123+
flags
124+
end
125+
106126
def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
107127
http = Net::HTTP.new(uri.host, uri.port)
108128
http.use_ssl = uri.scheme == 'https'
@@ -336,8 +356,9 @@ def as_otlp_span(span_data) # rubocop:disable Metrics/MethodLength, Metrics/Cycl
336356
trace_id: link.span_context.trace_id,
337357
span_id: link.span_context.span_id,
338358
trace_state: link.span_context.tracestate.to_s,
339-
attributes: link.attributes&.map { |k, v| as_otlp_key_value(k, v) }
359+
attributes: link.attributes&.map { |k, v| as_otlp_key_value(k, v) },
340360
# TODO: track dropped_attributes_count in Span#trim_links
361+
flags: build_span_flags(link.span_context.remote?, link.span_context.trace_flags)
341362
)
342363
end,
343364
dropped_links_count: span_data.total_recorded_links - span_data.links&.size.to_i,
@@ -346,7 +367,8 @@ def as_otlp_span(span_data) # rubocop:disable Metrics/MethodLength, Metrics/Cycl
346367
code: as_otlp_status_code(status.code),
347368
message: status.description
348369
)
349-
end
370+
end,
371+
flags: build_span_flags(span_data.parent_span_is_remote, span_data.trace_flags)
350372
)
351373
end
352374

exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,10 @@
672672
kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::SPAN_KIND_INTERNAL,
673673
start_time_unix_nano: (start_timestamp.to_r * 1_000_000_000).to_i,
674674
end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
675+
flags: (
676+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
677+
1
678+
),
675679
status: Opentelemetry::Proto::Trace::V1::Status.new(
676680
code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::STATUS_CODE_OK
677681
)
@@ -684,6 +688,10 @@
684688
kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::SPAN_KIND_CLIENT,
685689
start_time_unix_nano: ((start_timestamp + 2).to_r * 1_000_000_000).to_i,
686690
end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
691+
flags: (
692+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
693+
1
694+
),
687695
status: Opentelemetry::Proto::Trace::V1::Status.new(
688696
code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::STATUS_CODE_UNSET
689697
)
@@ -696,6 +704,10 @@
696704
kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::SPAN_KIND_CONSUMER,
697705
start_time_unix_nano: ((start_timestamp + 5).to_r * 1_000_000_000).to_i,
698706
end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
707+
flags: (
708+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
709+
1
710+
),
699711
status: Opentelemetry::Proto::Trace::V1::Status.new(
700712
code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::STATUS_CODE_UNSET
701713
)
@@ -740,11 +752,19 @@
740752
span_id: root_span_id,
741753
attributes: [
742754
Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4))
743-
]
755+
],
756+
flags: (
757+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
758+
1
759+
)
744760
)
745761
],
746762
status: Opentelemetry::Proto::Trace::V1::Status.new(
747763
code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::STATUS_CODE_ERROR
764+
),
765+
flags: (
766+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
767+
1
748768
)
749769
)
750770
]
@@ -762,6 +782,10 @@
762782
kind: Opentelemetry::Proto::Trace::V1::Span::SpanKind::SPAN_KIND_SERVER,
763783
start_time_unix_nano: ((start_timestamp + 3).to_r * 1_000_000_000).to_i,
764784
end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
785+
flags: (
786+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
787+
1
788+
),
765789
status: Opentelemetry::Proto::Trace::V1::Status.new(
766790
code: Opentelemetry::Proto::Trace::V1::Status::StatusCode::STATUS_CODE_UNSET
767791
)
@@ -779,4 +803,136 @@
779803
end
780804
end
781805
end
806+
807+
describe 'span flags' do
808+
let(:exporter) { OpenTelemetry::Exporter::OTLP::Exporter.new }
809+
let(:trace_id) { OpenTelemetry::Trace.generate_trace_id }
810+
let(:span_id) { OpenTelemetry::Trace.generate_span_id }
811+
let(:parent_span_id) { OpenTelemetry::Trace.generate_span_id }
812+
let(:resource) { OpenTelemetry::SDK::Resources::Resource.create('service.name' => 'test-service') }
813+
let(:instrumentation_scope) { OpenTelemetry::SDK::InstrumentationScope.new('test-lib', '1.0.0') }
814+
815+
describe 'build_span_flags' do
816+
it 'sets flags to HAS_IS_REMOTE for local parent span context' do
817+
flags = exporter.send(:build_span_flags, false, OpenTelemetry::Trace::TraceFlags::DEFAULT)
818+
_(flags).must_equal(
819+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
820+
)
821+
end
822+
823+
it 'sets flags to HAS_IS_REMOTE | IS_REMOTE for remote parent span context' do
824+
flags = exporter.send(:build_span_flags, true, OpenTelemetry::Trace::TraceFlags::DEFAULT)
825+
_(flags).must_equal(
826+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
827+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
828+
)
829+
end
830+
831+
it 'preserves base trace flags' do
832+
flags = exporter.send(:build_span_flags, false, OpenTelemetry::Trace::TraceFlags::SAMPLED)
833+
_(flags).must_equal(
834+
0x01 |
835+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
836+
)
837+
end
838+
end
839+
840+
describe 'as_otlp_span with flags' do
841+
it 'sets flags to HAS_IS_REMOTE for local parent span context' do
842+
span_data = create_span_data(parent_span_is_remote: false)
843+
span = exporter.send(:as_otlp_span, span_data)
844+
_(span.flags).must_equal(
845+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
846+
)
847+
end
848+
849+
it 'sets flags to HAS_IS_REMOTE | IS_REMOTE for remote parent span context' do
850+
span_data = create_span_data(parent_span_is_remote: true)
851+
span = exporter.send(:as_otlp_span, span_data)
852+
_(span.flags).must_equal(
853+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
854+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
855+
)
856+
end
857+
end
858+
859+
describe 'as_otlp_span with link flags' do
860+
it 'sets link flags to HAS_IS_REMOTE for local link context' do
861+
local_span_context = OpenTelemetry::Trace::SpanContext.new(trace_id: trace_id, span_id: parent_span_id, remote: false)
862+
local_link = create_link(local_span_context)
863+
span_data = create_span_data(links: [local_link])
864+
span = exporter.send(:as_otlp_span, span_data)
865+
_(span.links.first.flags).must_equal(
866+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK
867+
)
868+
end
869+
870+
it 'sets link flags to HAS_IS_REMOTE | IS_REMOTE for remote link context' do
871+
remote_span_context = OpenTelemetry::Trace::SpanContext.new(trace_id: trace_id, span_id: parent_span_id, remote: true)
872+
remote_link = create_link(remote_span_context)
873+
span_data = create_span_data(links: [remote_link])
874+
span = exporter.send(:as_otlp_span, span_data)
875+
_(span.links.first.flags).must_equal(
876+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
877+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
878+
)
879+
end
880+
end
881+
882+
describe 'export with flags' do
883+
it 'includes flags in exported spans' do
884+
span_data = create_span_data(parent_span_is_remote: true)
885+
encoded_data = exporter.send(:encode, [span_data])
886+
decoded = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(encoded_data)
887+
exported_span = decoded.resource_spans.first.scope_spans.first.spans.first
888+
_(exported_span.flags).must_equal(
889+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
890+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
891+
)
892+
end
893+
894+
it 'includes flags in exported links' do
895+
remote_span_context = OpenTelemetry::Trace::SpanContext.new(trace_id: trace_id, span_id: parent_span_id, remote: true)
896+
remote_link = create_link(remote_span_context)
897+
span_data = create_span_data(links: [remote_link])
898+
encoded_data = exporter.send(:encode, [span_data])
899+
decoded = Opentelemetry::Proto::Collector::Trace::V1::ExportTraceServiceRequest.decode(encoded_data)
900+
exported_link = decoded.resource_spans.first.scope_spans.first.spans.first.links.first
901+
_(exported_link.flags).must_equal(
902+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK |
903+
Opentelemetry::Proto::Trace::V1::SpanFlags::SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK
904+
)
905+
end
906+
end
907+
908+
private
909+
910+
def create_span_data(parent_span_is_remote: false, links: [])
911+
OpenTelemetry::SDK::Trace::SpanData.new(
912+
'test-span', # name
913+
:internal, # kind
914+
OpenTelemetry::Trace::Status.ok, # status
915+
parent_span_id, # parent_span_id
916+
0, # total_recorded_attributes
917+
0, # total_recorded_events
918+
links.size, # total_recorded_links
919+
Time.now.to_i * 1_000_000_000, # start_timestamp
920+
Time.now.to_i * 1_000_000_000, # end_timestamp
921+
{}, # attributes
922+
links, # links
923+
[], # events
924+
resource, # resource
925+
instrumentation_scope, # instrumentation_scope
926+
span_id, # span_id
927+
trace_id, # trace_id
928+
OpenTelemetry::Trace::TraceFlags::DEFAULT, # trace_flags
929+
OpenTelemetry::Trace::Tracestate::DEFAULT, # tracestate
930+
parent_span_is_remote # parent_span_is_remote
931+
)
932+
end
933+
934+
def create_link(span_context)
935+
OpenTelemetry::Trace::Link.new(span_context, { 'link-attribute' => 'link-value' })
936+
end
937+
end
782938
end

sdk/lib/opentelemetry/sdk/trace/span.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,19 @@ def to_span_data
312312
context.span_id,
313313
context.trace_id,
314314
context.trace_flags,
315-
context.tracestate
315+
context.tracestate,
316+
@parent_span_is_remote
316317
)
317318
end
318319

319320
# @api private
320-
def initialize(context, parent_context, parent_span, name, kind, parent_span_id, span_limits, span_processors, attributes, links, start_timestamp, resource, instrumentation_scope) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
321+
def initialize(context, parent_context, parent_span, name, kind, parent_span_id, span_limits, span_processors, attributes, links, start_timestamp, resource, instrumentation_scope) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
321322
super(span_context: context)
322323
@mutex = Mutex.new
323324
@name = name
324325
@kind = kind
325326
@parent_span_id = parent_span_id.freeze || OpenTelemetry::Trace::INVALID_SPAN_ID
327+
@parent_span_is_remote = parent_span&.context&.remote? || false
326328
@span_limits = span_limits
327329
@span_processors = span_processors
328330
@resource = resource

sdk/lib/opentelemetry/sdk/trace/span_data.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ module Trace
2727
:span_id, # String (8 byte binary)
2828
:trace_id, # String (16-byte binary)
2929
:trace_flags, # Integer (8-bit byte of bit flags)
30-
:tracestate) do # OpenTelemetry::Trace::Tracestate
30+
:tracestate, # OpenTelemetry::Trace::Tracestate
31+
:parent_span_is_remote) do # Boolean (whether parent span context is remote)
3132
# Returns the lowercase [hex encoded](https://tools.ietf.org/html/rfc4648#section-8) span ID.
3233
#
3334
# @return [String] A 16-hex-character lowercase string.

0 commit comments

Comments
 (0)