diff --git a/lib/instana/instrumentation/instrumented_request.rb b/lib/instana/instrumentation/instrumented_request.rb index cf5ad31e..fcfa041a 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 diff --git a/lib/instana/instrumentation/net-http.rb b/lib/instana/instrumentation/net-http.rb index 4a07b252..2301a266 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 = ::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 current_span&.record_exception(e) diff --git a/lib/instana/instrumentation/rack.rb b/lib/instana/instrumentation/rack.rb index 40c8c3eb..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,75 +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 - + 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 @@ -112,7 +51,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 @@ -121,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 diff --git a/lib/instana/trace/span_context.rb b/lib/instana/trace/span_context.rb index 00e96c40..08859f36 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/lib/instana/util.rb b/lib/instana/util.rb index 28f49fbd..f82e2940 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_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] = req_res_headers[custom_header] if req_res_headers.key?(custom_header) + end + + headers + end end end 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 }