From 4cc3ee465b8ee6f6b4923513d5ec286a887c8d21 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Tue, 4 Nov 2025 12:18:29 +0530 Subject: [PATCH 1/7] feat: add method to extract extra headers Signed-off-by: Arjun Rajappa --- lib/instana/util.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/instana/util.rb b/lib/instana/util.rb index 28f49fbd..9bd7313b 100644 --- a/lib/instana/util.rb +++ b/lib/instana/util.rb @@ -181,6 +181,19 @@ def maybe_timeout(timeout, start_time) timeout -= (timeout_timestamp - start_time) timeout.positive? ? timeout : 0 end + + def extra_response_header_tags(incoming_headers) + return {} if incoming_headers.nil? + return nil unless ::Instana.agent.extra_headers + + headers = {} + + ::Instana.agent.extra_headers.each do |custom_header| + headers[custom_header.to_sym] = incoming_headers[custom_header] if incoming_headers.key?(custom_header) + end + + headers + end end end end From bf21e50c1e092ba9d1bb5ddd013e0e203eb9df48 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Wed, 5 Nov 2025 16:30:55 +0530 Subject: [PATCH 2/7] feat: add request response headers to net-http Signed-off-by: Arjun Rajappa --- lib/instana/instrumentation/net-http.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/instana/instrumentation/net-http.rb b/lib/instana/instrumentation/net-http.rb index 4a07b252..4b3ecf02 100644 --- a/lib/instana/instrumentation/net-http.rb +++ b/lib/instana/instrumentation/net-http.rb @@ -56,7 +56,8 @@ def request(*args, &block) # without a backtrace (no exception) current_span.record_exception(nil) end - + extra_headers = extra_header_tags(response)&.merge(extra_header_tags(request)) + kv_payload[:http][:header] = extra_headers unless extra_headers&.empty? response rescue => e current_span&.record_exception(e) @@ -71,6 +72,20 @@ def skip_instrumentation? !Instana.tracer.tracing? || !started? || !Instana.config[:nethttp][:enabled] || (!::Instana.tracer.current_span.nil? && dnt_spans.include?(::Instana.tracer.current_span.name)) end + + def extra_header_tags(request_response) + return nil unless ::Instana.agent.extra_headers + + headers = {} + + ::Instana.agent.extra_headers.each do |custom_header| + # Headers are available in this format: HTTP_X_CAPTURE_THIS + + headers[custom_header.to_sym] = request_response[custom_header] if request_response.key?(custom_header) + end + + headers + end end end end From c62d482bf836fec65c2cccb8bba32887b8758204 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Wed, 5 Nov 2025 19:14:38 +0530 Subject: [PATCH 3/7] feat: add request response headers to rack Signed-off-by: Arjun Rajappa --- .../instrumentation/instrumented_request.rb | 16 ++++++++++++++++ lib/instana/instrumentation/rack.rb | 10 ++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/instana/instrumentation/instrumented_request.rb b/lib/instana/instrumentation/instrumented_request.rb index cf5ad31e..a3e43d65 100644 --- a/lib/instana/instrumentation/instrumented_request.rb +++ b/lib/instana/instrumentation/instrumented_request.rb @@ -122,6 +122,7 @@ def context_from_instana_headers long_instana_id: long_instana_id? ? sanitized_t : nil, external_trace_id: external_trace_id, external_state: @env['HTTP_TRACESTATE'], + external_trace_flags: context_from_trace_parent[:external_trace_flags], from_w3c: false }.reject { |_, v| v.nil? } end @@ -140,6 +141,7 @@ def context_from_trace_parent external_state: @env['HTTP_TRACESTATE'], trace_id: trace_id, span_id: span_id, + external_trace_flags: matches['flags'], from_w3c: true } end @@ -186,5 +188,19 @@ def parse_correlation_data id: data['correlationId'] }.reject { |_, v| v.nil? } end + + def extra_response_header_tags(response) + return nil unless ::Instana.agent.extra_headers + + headers = {} + + ::Instana.agent.extra_headers.each do |custom_header| + # Headers are available in this format: HTTP_X_CAPTURE_THIS + + headers[custom_header.to_sym] = response[custom_header] if response.key?(custom_header) + end + + headers + end end end diff --git a/lib/instana/instrumentation/rack.rb b/lib/instana/instrumentation/rack.rb index 40c8c3eb..28a97fb4 100644 --- a/lib/instana/instrumentation/rack.rb +++ b/lib/instana/instrumentation/rack.rb @@ -69,7 +69,12 @@ def call(env) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLengt # them in the response headers in the ensure block trace_context = ::Instana.tracer.current_span.context end - + extra_response_headers = ::Instana::Util.extra_response_header_tags(headers) + if kvs[:http][:header].nil? + kvs[:http][:header] = extra_response_headers + else + kvs[:http][:header].merge!(extra_response_headers) + end [status, headers, response] rescue Exception => e current_span.record_exception(e) if ::Instana.tracer.tracing? @@ -112,7 +117,8 @@ def extract_trace_context(incoming_context) level: incoming_context[:level], baggage: { external_trace_id: incoming_context[:external_trace_id], - external_state: incoming_context[:external_state] + external_state: incoming_context[:external_state], + external_trace_flags: incoming_context[:external_trace_flags] } ) end From 677f3c540a0c8890d9ce9ab4fc11a3e0f1e16b7f Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Thu, 6 Nov 2025 14:32:10 +0530 Subject: [PATCH 4/7] feat: trace flags improvements - consider all the possible combination Signed-off-by: Arjun Rajappa --- lib/instana/trace/span_context.rb | 12 ++++++++++-- .../rack_instrumented_request_test.rb | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/instana/trace/span_context.rb b/lib/instana/trace/span_context.rb index 00e96c40..f0b2dd61 100644 --- a/lib/instana/trace/span_context.rb +++ b/lib/instana/trace/span_context.rb @@ -42,8 +42,16 @@ def span_id_header def trace_parent_header trace = (@baggage[:external_trace_id] || trace_id_header).rjust(32, '0') parent = span_id_header.rjust(16, '0') - flags = @level == 1 ? "03" : "02" - + flags = if @baggage[:external_trace_flags] + # Parse external flags as 8-bit hex, clear LSB, then set LSB based on level + external_flags = @baggage[:external_trace_flags].to_i(16) & 0xFE # Clear LSB + combined_flags = external_flags | (@level == 1 ? 1 : 0) # Set LSB based on level + combined_flags = [combined_flags, 0xFF].min # Cap at 8-bit max + format('%02x', combined_flags) + else + @level == 1 ? "03" : "02" + end + flags = "03" if flags > "03" "00-#{trace}-#{parent}-#{flags}" end diff --git a/test/instrumentation/rack_instrumented_request_test.rb b/test/instrumentation/rack_instrumented_request_test.rb index ab8dac73..3be62406 100644 --- a/test/instrumentation/rack_instrumented_request_test.rb +++ b/test/instrumentation/rack_instrumented_request_test.rb @@ -69,6 +69,7 @@ def test_incoming_w3c_context external_state: nil, trace_id: 'a3ce929d0e0e4736', span_id: '00f067aa0ba902b7', + external_trace_flags: "01", from_w3c: true } @@ -86,6 +87,7 @@ def test_incoming_w3c_context_newer_version_additional_fields external_state: nil, trace_id: 'a3ce929d0e0e4736', span_id: '00f067aa0ba902b7', + external_trace_flags: "01", from_w3c: true } @@ -103,6 +105,7 @@ def test_incoming_w3c_context_unknown_flags external_state: nil, trace_id: 'a3ce929d0e0e4736', span_id: '00f067aa0ba902b7', + external_trace_flags: "ff", from_w3c: true } From d3e41cb1590f54bd9e7dad0dbed0e51bc5d56d81 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Fri, 7 Nov 2025 12:48:19 +0530 Subject: [PATCH 5/7] feat: move header extraction logic to one place Signed-off-by: Arjun Rajappa --- .../instrumentation/instrumented_request.rb | 14 -------------- lib/instana/instrumentation/net-http.rb | 16 +--------------- lib/instana/instrumentation/rack.rb | 2 +- lib/instana/util.rb | 6 +++--- 4 files changed, 5 insertions(+), 33 deletions(-) diff --git a/lib/instana/instrumentation/instrumented_request.rb b/lib/instana/instrumentation/instrumented_request.rb index a3e43d65..fcfa041a 100644 --- a/lib/instana/instrumentation/instrumented_request.rb +++ b/lib/instana/instrumentation/instrumented_request.rb @@ -188,19 +188,5 @@ def parse_correlation_data id: data['correlationId'] }.reject { |_, v| v.nil? } end - - def extra_response_header_tags(response) - return nil unless ::Instana.agent.extra_headers - - headers = {} - - ::Instana.agent.extra_headers.each do |custom_header| - # Headers are available in this format: HTTP_X_CAPTURE_THIS - - headers[custom_header.to_sym] = response[custom_header] if response.key?(custom_header) - end - - headers - end end end diff --git a/lib/instana/instrumentation/net-http.rb b/lib/instana/instrumentation/net-http.rb index 4b3ecf02..2301a266 100644 --- a/lib/instana/instrumentation/net-http.rb +++ b/lib/instana/instrumentation/net-http.rb @@ -56,7 +56,7 @@ def request(*args, &block) # without a backtrace (no exception) current_span.record_exception(nil) end - extra_headers = extra_header_tags(response)&.merge(extra_header_tags(request)) + extra_headers = ::Instana::Util.extra_header_tags(response)&.merge(::Instana::Util.extra_header_tags(request)) kv_payload[:http][:header] = extra_headers unless extra_headers&.empty? response rescue => e @@ -72,20 +72,6 @@ def skip_instrumentation? !Instana.tracer.tracing? || !started? || !Instana.config[:nethttp][:enabled] || (!::Instana.tracer.current_span.nil? && dnt_spans.include?(::Instana.tracer.current_span.name)) end - - def extra_header_tags(request_response) - return nil unless ::Instana.agent.extra_headers - - headers = {} - - ::Instana.agent.extra_headers.each do |custom_header| - # Headers are available in this format: HTTP_X_CAPTURE_THIS - - headers[custom_header.to_sym] = request_response[custom_header] if request_response.key?(custom_header) - end - - headers - end end end end diff --git a/lib/instana/instrumentation/rack.rb b/lib/instana/instrumentation/rack.rb index 28a97fb4..49adf377 100644 --- a/lib/instana/instrumentation/rack.rb +++ b/lib/instana/instrumentation/rack.rb @@ -69,7 +69,7 @@ def call(env) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLengt # them in the response headers in the ensure block trace_context = ::Instana.tracer.current_span.context end - extra_response_headers = ::Instana::Util.extra_response_header_tags(headers) + extra_response_headers = ::Instana::Util.extra_header_tags(headers) if kvs[:http][:header].nil? kvs[:http][:header] = extra_response_headers else diff --git a/lib/instana/util.rb b/lib/instana/util.rb index 9bd7313b..f82e2940 100644 --- a/lib/instana/util.rb +++ b/lib/instana/util.rb @@ -182,14 +182,14 @@ def maybe_timeout(timeout, start_time) timeout.positive? ? timeout : 0 end - def extra_response_header_tags(incoming_headers) - return {} if incoming_headers.nil? + def extra_header_tags(req_res_headers) + return {} if req_res_headers.nil? return nil unless ::Instana.agent.extra_headers headers = {} ::Instana.agent.extra_headers.each do |custom_header| - headers[custom_header.to_sym] = incoming_headers[custom_header] if incoming_headers.key?(custom_header) + headers[custom_header.to_sym] = req_res_headers[custom_header] if req_res_headers.key?(custom_header) end headers From 148a2275b5ea58c12069a0ac93b6c3db7da97b31 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Fri, 7 Nov 2025 13:38:48 +0530 Subject: [PATCH 6/7] lint: fix linting failures Signed-off-by: Arjun Rajappa --- lib/instana/trace/span_context.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/instana/trace/span_context.rb b/lib/instana/trace/span_context.rb index f0b2dd61..08859f36 100644 --- a/lib/instana/trace/span_context.rb +++ b/lib/instana/trace/span_context.rb @@ -44,9 +44,9 @@ def trace_parent_header parent = span_id_header.rjust(16, '0') flags = if @baggage[:external_trace_flags] # Parse external flags as 8-bit hex, clear LSB, then set LSB based on level - external_flags = @baggage[:external_trace_flags].to_i(16) & 0xFE # Clear LSB - combined_flags = external_flags | (@level == 1 ? 1 : 0) # Set LSB based on level - combined_flags = [combined_flags, 0xFF].min # Cap at 8-bit max + external_flags = @baggage[:external_trace_flags].to_i(16) & 0xFE # Clear LSB + combined_flags = external_flags | (@level == 1 ? 1 : 0) # Set LSB based on level + combined_flags = [combined_flags, 0xFF].min # Cap at 8-bit max format('%02x', combined_flags) else @level == 1 ? "03" : "02" From c0ceb95049c8dbb5927591994a92d38789cab2d1 Mon Sep 17 00:00:00 2001 From: Arjun Rajappa Date: Fri, 7 Nov 2025 17:34:02 +0530 Subject: [PATCH 7/7] lint: refactor rack instrumentation to reduce complexity Signed-off-by: Arjun Rajappa --- lib/instana/instrumentation/rack.rb | 154 +++++++++++++++------------- 1 file changed, 84 insertions(+), 70 deletions(-) diff --git a/lib/instana/instrumentation/rack.rb b/lib/instana/instrumentation/rack.rb index 49adf377..11b81a7a 100644 --- a/lib/instana/instrumentation/rack.rb +++ b/lib/instana/instrumentation/rack.rb @@ -10,7 +10,7 @@ def initialize(app) @app = app end - def call(env) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength + def call(env) req = InstrumentedRequest.new(env) kvs = { http: req.request_tags @@ -26,80 +26,14 @@ def call(env) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLengt @trace_token = OpenTelemetry::Context.attach(trace_ctx) status, headers, response = @app.call(env) - if ::Instana.tracer.tracing? - unless req.correlation_data.empty? - current_span[:crid] = req.correlation_data[:id] - current_span[:crtp] = req.correlation_data[:type] - end - - if !req.instana_ancestor.empty? && req.continuing_from_trace_parent? - current_span[:ia] = req.instana_ancestor - end - - if req.continuing_from_trace_parent? - current_span[:tp] = true - end - - if req.external_trace_id? - current_span[:lt] = req.external_trace_id - end - - if req.synthetic? - current_span[:sy] = true - end - - # In case some previous middleware returned a string status, make sure that we're dealing with - # an integer. In Ruby nil.to_i, "asdfasdf".to_i will always return 0 from Ruby versions 1.8.7 and newer. - # So if an 0 status is reported here, it indicates some other issue (e.g. no status from previous middleware) - # See Rack Spec: https://www.rubydoc.info/github/rack/rack/file/SPEC#label-The+Status - kvs[:http][:status] = status.to_i - - if status.to_i >= 500 - # Because of the 5xx response, we flag this span as errored but - # without a backtrace (no exception) - ::Instana.tracer.log_error(nil) - end - - # If the framework instrumentation provides a path template, - # pass it into the span here. - # See: https://www.instana.com/docs/tracing/custom-best-practices/#path-templates-visual-grouping-of-http-endpoints - kvs[:http][:path_tpl] = env['INSTANA_HTTP_PATH_TEMPLATE'] if env['INSTANA_HTTP_PATH_TEMPLATE'] - - # Save the span context before the trace ends so we can place - # them in the response headers in the ensure block - trace_context = ::Instana.tracer.current_span.context - end - extra_response_headers = ::Instana::Util.extra_header_tags(headers) - if kvs[:http][:header].nil? - kvs[:http][:header] = extra_response_headers - else - kvs[:http][:header].merge!(extra_response_headers) - end + trace_context = process_span_tags(req, current_span, kvs, status, env) if ::Instana.tracer.tracing? + merge_response_headers(kvs, headers) [status, headers, response] rescue Exception => e current_span.record_exception(e) if ::Instana.tracer.tracing? raise ensure - if ::Instana.tracer.tracing? - if headers - # Set response headers; encode as hex string - if trace_context.active? - headers['X-Instana-T'] = trace_context.trace_id_header - headers['X-Instana-S'] = trace_context.span_id_header - headers['X-Instana-L'] = '1' - - headers['Tracestate'] = trace_context.trace_state_header - else - headers['X-Instana-L'] = '0' - end - - headers['Traceparent'] = trace_context.trace_parent_header - headers['Server-Timing'] = "intid;desc=#{trace_context.trace_id_header}" - end - current_span.set_tags(kvs) - OpenTelemetry::Context.detach(@trace_token) if @trace_token - current_span.finish - end + finalize_trace(current_span, kvs, headers, trace_context) if ::Instana.tracer.tracing? end private @@ -127,5 +61,85 @@ def extract_trace_context(incoming_context) end parent_context end + + def process_span_tags(req, current_span, kvs, status, env) + add_correlation_data(req, current_span) + add_trace_parent_data(req, current_span) + add_status_and_error(kvs, status) + add_path_template(kvs, env) + + # Save the span context before the trace ends so we can place + # them in the response headers in the ensure block + ::Instana.tracer.current_span.context + end + + def add_correlation_data(req, current_span) + return if req.correlation_data.empty? + + current_span[:crid] = req.correlation_data[:id] + current_span[:crtp] = req.correlation_data[:type] + end + + def add_trace_parent_data(req, current_span) + if !req.instana_ancestor.empty? && req.continuing_from_trace_parent? + current_span[:ia] = req.instana_ancestor + end + + current_span[:tp] = true if req.continuing_from_trace_parent? + current_span[:lt] = req.external_trace_id if req.external_trace_id? + current_span[:sy] = true if req.synthetic? + end + + def add_status_and_error(kvs, status) + # In case some previous middleware returned a string status, make sure that we're dealing with + # an integer. In Ruby nil.to_i, "asdfasdf".to_i will always return 0 from Ruby versions 1.8.7 and newer. + # So if an 0 status is reported here, it indicates some other issue (e.g. no status from previous middleware) + # See Rack Spec: https://www.rubydoc.info/github/rack/rack/file/SPEC#label-The+Status + kvs[:http][:status] = status.to_i + + return unless status.to_i >= 500 + + # Because of the 5xx response, we flag this span as errored but + # without a backtrace (no exception) + ::Instana.tracer.log_error(nil) + end + + def add_path_template(kvs, env) + # If the framework instrumentation provides a path template, + # pass it into the span here. + # See: https://www.instana.com/docs/tracing/custom-best-practices/#path-templates-visual-grouping-of-http-endpoints + kvs[:http][:path_tpl] = env['INSTANA_HTTP_PATH_TEMPLATE'] if env['INSTANA_HTTP_PATH_TEMPLATE'] + end + + def merge_response_headers(kvs, headers) + extra_response_headers = ::Instana::Util.extra_header_tags(headers) + if kvs[:http][:header].nil? + kvs[:http][:header] = extra_response_headers + else + kvs[:http][:header].merge!(extra_response_headers) + end + end + + def finalize_trace(current_span, kvs, headers, trace_context) + set_response_headers(headers, trace_context) if headers + current_span.set_tags(kvs) + OpenTelemetry::Context.detach(@trace_token) if @trace_token + current_span.finish + end + + def set_response_headers(headers, trace_context) + # Set response headers; encode as hex string + if trace_context.active? + headers['X-Instana-T'] = trace_context.trace_id_header + headers['X-Instana-S'] = trace_context.span_id_header + headers['X-Instana-L'] = '1' + headers['Tracestate'] = trace_context.trace_state_header + else + headers['X-Instana-L'] = '0' + end + + headers['Traceparent'] = trace_context.trace_parent_header + headers['Server-Timing'] = "intid;desc=#{trace_context.trace_id_header}" + end end end