diff --git a/lib/instana/config.rb b/lib/instana/config.rb index 73d1d9c0..9b497ab7 100644 --- a/lib/instana/config.rb +++ b/lib/instana/config.rb @@ -83,6 +83,10 @@ def initialize(logger: ::Instana.logger, agent_host: ENV.fetch('INSTANA_AGENT_HO # W3C Trace Context Support @config[:w3c_trace_correlation] = ENV.fetch('INSTANA_DISABLE_W3C_TRACE_CORRELATION', nil).nil? + # Server-Timing response header injection (default: injected) + # Priority: YAML > env var > agent config > default (false) + read_disable_server_timing_config + # HTTP exit span 4xx error classification (opt-in, default: off) # Priority: env vars > agent config > default (false / []) read_http_exit_config @@ -151,6 +155,8 @@ def read_config_from_agent(discovery) read_otlp_config_from_agent(tracing_config) if should_read_from_agent?(:otlp) # Read HTTP exit 4xx classification from agent if env vars did not already set it read_http_exit_classification_from_agent(tracing_config) unless env_http_exit_classification_set? + # Read Server-Timing disable flag from agent if not already set from YAML or env + read_disable_server_timing_from_agent(tracing_config) if @config[:disable_server_timing_source] == 'default' # Read span filtering configuration from agent ::Instana.span_filtering_config&.read_config_from_agent(discovery) rescue => e @@ -268,6 +274,49 @@ def read_otlp_config_from_agent(tracing_config) private + # Read disable-server-timing from YAML (tracing.global) or INSTANA_TRACING_DISABLE_SERVER_TIMING. + # Agent value is applied later via read_config_from_agent if neither is set. + def read_disable_server_timing_config + @config[:disable_server_timing] = false + @config[:disable_server_timing_source] = 'default' + + yaml_value = parse_disable_server_timing_from_yaml + unless yaml_value.nil? + @config[:disable_server_timing] = truthy?(yaml_value) + @config[:disable_server_timing_source] = 'yaml' + return + end + + env_value = ENV.fetch('INSTANA_TRACING_DISABLE_SERVER_TIMING', nil) + return if env_value.nil? || env_value.strip.empty? + + @config[:disable_server_timing] = truthy?(env_value.strip) + @config[:disable_server_timing_source] = 'env' + end + + # @return [Object, nil] raw YAML value of tracing.global.disable-server-timing, or nil if not set + def parse_disable_server_timing_from_yaml + config_path = ENV.fetch('INSTANA_CONFIG_PATH', nil) + return nil unless config_path && File.exist?(config_path) + + yaml_content = YAML.safe_load(File.read(config_path)) + tracing_config = yaml_content[TRACING_KEY] || yaml_content[LEGACY_TRACING_KEY] + global_config = tracing_config && tracing_config['global'] + global_config.is_a?(Hash) ? global_config['disable-server-timing'] : nil + rescue => e + ::Instana.logger.warn("Failed to load disable-server-timing configuration from YAML: #{e.message}") + nil + end + + # @param tracing_config [Hash] the tracing sub-hash from the discovery payload + def read_disable_server_timing_from_agent(tracing_config) + global_config = tracing_config['global'] + return unless global_config.is_a?(Hash) && !global_config['disable-server-timing'].nil? + + @config[:disable_server_timing] = truthy?(global_config['disable-server-timing']) + @config[:disable_server_timing_source] = 'agent' + end + # Read HTTP exit 4xx error classification config (wrapper — same pattern as read_span_stack_config / read_otlp_config) def read_http_exit_config @config[:http_exit_classify_all_4xx_as_errors] = false diff --git a/lib/instana/instrumentation/rack.rb b/lib/instana/instrumentation/rack.rb index 90293d5e..27e78766 100644 --- a/lib/instana/instrumentation/rack.rb +++ b/lib/instana/instrumentation/rack.rb @@ -139,6 +139,8 @@ def set_response_headers(headers, trace_context) end headers['Traceparent'] = trace_context.trace_parent_header + return if ::Instana.config[:disable_server_timing] + headers['Server-Timing'] = "intid;desc=#{trace_context.trace_id_header}" end end diff --git a/test/config_test.rb b/test/config_test.rb index 135691e0..79977f02 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -1283,3 +1283,99 @@ def test_agent_config_ignores_invalid_codes assert_equal [401], subject[:http_exit_classify_as_errors] end end + +class DisableServerTimingConfigTest < Minitest::Test + YAML_FILE = 'test_disable_server_timing.yaml'.freeze + + def logger + Logger.new('/dev/null') + end + + def setup + ENV.delete('INSTANA_TRACING_DISABLE_SERVER_TIMING') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def teardown + ENV.delete('INSTANA_TRACING_DISABLE_SERVER_TIMING') + ENV.delete('INSTANA_CONFIG_PATH') + File.unlink(YAML_FILE) if File.exist?(YAML_FILE) + end + + def write_yaml(top_key, value) + File.write(YAML_FILE, "#{top_key}:\n global:\n disable-server-timing: #{value}\n") + ENV['INSTANA_CONFIG_PATH'] = YAML_FILE + end + + def agent_discovery(value) + { 'tracing' => { 'global' => { 'disable-server-timing' => value } } } + end + + def test_default_is_false + subject = Instana::Config.new(logger: logger) + assert_equal false, subject[:disable_server_timing] + end + + def test_env_truthy_values + %w[true 1 TRUE yes].each do |value| + ENV['INSTANA_TRACING_DISABLE_SERVER_TIMING'] = value + assert_equal true, Instana::Config.new(logger: logger)[:disable_server_timing], "expected #{value} to be truthy" + end + end + + def test_env_falsy_or_empty_values + ['false', '0', ''].each do |value| + ENV['INSTANA_TRACING_DISABLE_SERVER_TIMING'] = value + assert_equal false, Instana::Config.new(logger: logger)[:disable_server_timing], "expected #{value.inspect} to be falsy" + end + end + + def test_yaml_tracing_key + write_yaml('tracing', 'true') + assert_equal true, Instana::Config.new(logger: logger)[:disable_server_timing] + end + + def test_yaml_legacy_key + write_yaml('com.instana.tracing', 'true') + assert_equal true, Instana::Config.new(logger: logger)[:disable_server_timing] + end + + def test_yaml_takes_precedence_over_env + write_yaml('tracing', 'false') + ENV['INSTANA_TRACING_DISABLE_SERVER_TIMING'] = 'true' + assert_equal false, Instana::Config.new(logger: logger)[:disable_server_timing] + end + + def test_invalid_yaml_falls_back_to_env + File.write(YAML_FILE, "tracing: [unclosed") + ENV['INSTANA_CONFIG_PATH'] = YAML_FILE + ENV['INSTANA_TRACING_DISABLE_SERVER_TIMING'] = 'true' + assert_equal true, Instana::Config.new(logger: logger)[:disable_server_timing] + end + + def test_agent_sets_value_when_nothing_else_configured + subject = Instana::Config.new(logger: logger) + subject.read_config_from_agent(agent_discovery(true)) + assert_equal true, subject[:disable_server_timing] + end + + def test_env_takes_precedence_over_agent + ENV['INSTANA_TRACING_DISABLE_SERVER_TIMING'] = 'true' + subject = Instana::Config.new(logger: logger) + subject.read_config_from_agent(agent_discovery(false)) + assert_equal true, subject[:disable_server_timing] + end + + def test_yaml_takes_precedence_over_agent + write_yaml('tracing', 'true') + subject = Instana::Config.new(logger: logger) + subject.read_config_from_agent(agent_discovery(false)) + assert_equal true, subject[:disable_server_timing] + end + + def test_agent_without_key_keeps_default + subject = Instana::Config.new(logger: logger) + subject.read_config_from_agent('tracing' => { 'global' => { 'stack-trace' => 'all' } }) + assert_equal false, subject[:disable_server_timing] + end +end diff --git a/test/instrumentation/rack_test.rb b/test/instrumentation/rack_test.rb index 686a4b28..9b980299 100644 --- a/test/instrumentation/rack_test.rb +++ b/test/instrumentation/rack_test.rb @@ -33,6 +33,12 @@ def call(_env) end end + class ServerTimingApp + def call(_env) + [200, { 'Server-Timing' => 'db;dur=53' }, ['Ok']] + end + end + class NoHeadersApp def call(_env) [501, nil, ['No']] @@ -53,9 +59,36 @@ def app map("/path_tpl") { run PathTemplateApp.new } map("/error") { run ErrorApp.new } map("/five_zero_one") { run FiveZeroOneApp.new } + map("/server_timing") { run ServerTimingApp.new } end end + def test_server_timing_not_added_when_disabled + clear_all! + ::Instana.config[:disable_server_timing] = true + + get '/mrlobster' + assert last_response.ok? + + assert !last_response.headers.key?("Server-Timing") + assert last_response.headers.key?("X-Instana-T") + assert last_response.headers.key?("Traceparent") + ensure + ::Instana.config[:disable_server_timing] = false + end + + def test_existing_server_timing_preserved_when_disabled + clear_all! + ::Instana.config[:disable_server_timing] = true + + get '/server_timing' + assert last_response.ok? + + assert_equal 'db;dur=53', last_response.headers["Server-Timing"] + ensure + ::Instana.config[:disable_server_timing] = false + end + def test_basic_get clear_all! Instana.config[:back_trace][:stack_trace_level] = "all"