Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Rake::TestTask.new(:test) do |t|
else
t.test_files = Dir[
'test/*_test.rb',
'test/{agent,trace,backend,snapshot,span_filtering}/*_test.rb'
'test/{agent,trace,backend,snapshot,span_filtering,samplers}/*_test.rb'
]
end
end
Expand Down
8 changes: 7 additions & 1 deletion lib/instana/instrumentation/action_cable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ def instana_trace_context
end

def process
@instana_trace_context ||= ::Instana.tracer.tracing? ? ::Instana.tracer.current_span.context : {}
# Wrapped in non_recording_span by the channel callbacks below, so it
# must be a SpanContext.
@instana_trace_context ||= if ::Instana.tracer.tracing?
::Instana.tracer.current_span.context
else
OpenTelemetry::Trace::SpanContext::INVALID
end
super
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/instana/instrumentation/grpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ module GRPCServerInstrumentation
kvs = { rpc: {} }
metadata = active_call.metadata

incoming_context = {}
incoming_context = nil
if metadata.key?('x-instana-t')
incoming_context = SpanContext.new(trace_id: ::Instana::Util.header_to_id(metadata['x-instana-t']),
span_id: metadata.key?('x-instana-s') ? ::Instana::Util.header_to_id(metadata['x-instana-s']) : nil,
Expand Down
5 changes: 4 additions & 1 deletion lib/instana/samplers/samplers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ def self.trace_id_ratio_based(_)

def self.should_sample?(trace_id:, parent_context:, links:, name:, kind:, attributes:) # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument:
parent_span_context = OpenTelemetry::Trace.current_span(parent_context).context
tracestate = parent_span_context&.tracestate
# Parents wrapped in non_recording_span can expose a non-SpanContext
# #context; the new span's SpanContext needs a Tracestate, never nil.
tracestate = parent_span_context.tracestate if parent_span_context.respond_to?(:tracestate)
tracestate ||= OpenTelemetry::Trace::Tracestate::DEFAULT
Result.new(decision: :__record_only__, tracestate: tracestate)
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/instana/trace/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,9 @@ def clear!
# with_parent=current context, start_timestamp=current time.
#
def start_span(name, with_parent: nil, attributes: nil, links: nil, start_timestamp: ::Instana::Util.now_in_ms, kind: nil) # rubocop:disable Metrics/ParameterLists
return Instana::Trace.non_recording_span(with_parent) if !::Instana.agent.ready? || !::Instana.config[:tracing][:enabled]
# The placeholder's #context is read back as the parent by the next span
# start, so it must be a SpanContext (INVALID when there is no parent).
return Instana::Trace.non_recording_span(OpenTelemetry::Trace.current_span(with_parent).context) if !::Instana.agent.ready? || !::Instana.config[:tracing][:enabled]

with_parent ||= OpenTelemetry::Context.current
name ||= 'empty'
Expand Down
23 changes: 23 additions & 0 deletions test/instrumentation/grpc_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,29 @@ def test_bidi_streamer_failure
assert_equal client_span[:p], sdk_span[:s]
end

# A call from an untraced client carries no x-instana-t/-s metadata; the
# server still answers normally and records the call as a root span.
def test_server_span_for_request_without_trace_headers
clear_all!

response = client_stub.ping(
PingPongService::PingRequest.new(message: 'World')
)
sleep 0.2

assert_equal 'Hello World', response.message

server_span, rest = ::Instana.processor.queued_spans
assert_nil rest

assert_server_span(
server_span,
call: '/PingPongService/Ping',
call_type: :request_response
)
assert_nil server_span[:p]
end

def test_no_error_is_raised_and_no_spans_are_created_when_agent_is_not_ready
clear_all!
error = nil
Expand Down
48 changes: 48 additions & 0 deletions test/samplers/sampler_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# (c) Copyright IBM Corp. 2026

require 'test_helper'
require 'instana/samplers/samplers'

class SamplerTest < Minitest::Test
# A malformed parent (non_recording_span accepts any object, so a bare
# Context or Hash can surface here) does not crash span creation and still
# yields a real Tracestate, which is copied into the new span's context.
def test_should_sample_tolerates_parents_without_a_real_span_context
[OpenTelemetry::Context.empty, {}].each do |not_a_span_context|
poisoned_span = Instana::Trace.non_recording_span(not_a_span_context)
parent_context = OpenTelemetry::Trace.context_with_span(
poisoned_span, parent_context: OpenTelemetry::Context.empty
)

result = Instana::Trace::Samplers.should_sample?(
trace_id: 'trace-id', parent_context: parent_context,
links: nil, name: 'rack', kind: :server, attributes: nil
)

assert result.recording?
assert_instance_of OpenTelemetry::Trace::Tracestate, result.tracestate
end
end

# W3C tracestate received from upstream propagates to child spans
# unchanged. The parent's tracestate differs from the shared default, so a
# substituted default fails the assertion.
def test_should_sample_propagates_the_parent_tracestate
tracestate = OpenTelemetry::Trace::Tracestate.from_string('in=abc;def')
parent_span_context = Instana::Trace::SpanContext.new(
trace_id: 'a' * 32, span_id: 'b' * 16, tracestate: tracestate
)
parent_context = OpenTelemetry::Trace.context_with_span(
Instana::Trace.non_recording_span(parent_span_context),
parent_context: OpenTelemetry::Context.empty
)

result = Instana::Trace::Samplers.should_sample?(
trace_id: 'trace-id', parent_context: parent_context,
links: nil, name: 'rack', kind: :server, attributes: nil
)

assert result.recording?
assert_equal 'abc;def', result.tracestate.value('in')
end
end
57 changes: 57 additions & 0 deletions test/trace/tracer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,60 @@ def test_missing_class_super
end
end
end

# While the agent is starting up (or tracing is disabled), start_span
# returns a non-recording placeholder span whose #context the next span
# start in the request reads back as its parent.
class TracerStartSpanTest < Minitest::Test
# A request arriving with upstream trace headers during agent warmup keeps
# the upstream ids, so spans recorded later join the upstream trace.
def test_start_span_when_agent_not_ready_preserves_the_incoming_span_context
incoming = Instana::Trace::SpanContext.new(trace_id: 'a' * 32, span_id: 'b' * 16)
with_parent = OpenTelemetry::Trace.context_with_span(
Instana::Trace.non_recording_span(incoming),
parent_context: OpenTelemetry::Context.empty
)

span = ::Instana.agent.stub(:ready?, false) do
::Instana.tracer.start_span(:rack, with_parent: with_parent)
end

refute_kind_of OpenTelemetry::Context, span.context
assert_equal incoming.trace_id, span.context.trace_id
assert_equal incoming.span_id, span.context.span_id
assert_equal incoming.tracestate, span.context.tracestate
end

# A library call that starts a span without naming a parent inherits the
# trace already active on the thread.
def test_start_span_when_agent_not_ready_defaults_to_the_current_context
ambient = Instana::Trace::SpanContext.new(trace_id: 'c' * 32, span_id: 'd' * 16)
context = OpenTelemetry::Trace.context_with_span(
Instana::Trace.non_recording_span(ambient),
parent_context: OpenTelemetry::Context.empty
)

span = OpenTelemetry::Context.with_current(context) do
::Instana.agent.stub(:ready?, false) do
::Instana.tracer.start_span(:rack)
end
end

assert_equal ambient.trace_id, span.context.trace_id
assert_equal ambient.span_id, span.context.span_id
end

# With no incoming trace and none active on the thread, the placeholder
# reports an invalid context, since a fabricated valid trace id would
# become the parent of real spans once the agent comes online, breaking
# those traces.
def test_start_span_when_agent_not_ready_and_no_parent_returns_an_invalid_span_context
span = OpenTelemetry::Context.with_current(OpenTelemetry::Context.empty) do
::Instana.agent.stub(:ready?, false) do
::Instana.tracer.start_span(:rack)
end
end

refute span.context.valid?
end
end
Loading