diff --git a/lib/instana/backend/host_agent_activation_observer.rb b/lib/instana/backend/host_agent_activation_observer.rb index e1bd46ef..56b2157e 100644 --- a/lib/instana/backend/host_agent_activation_observer.rb +++ b/lib/instana/backend/host_agent_activation_observer.rb @@ -40,6 +40,9 @@ def update(_time, _old_version, new_version) wait_for_backend(discovery['pid']) @logger.debug("Agent ready.") @discovery.swap { discovery } + + # Read configuration from agent right after discovery + ::Instana.config.read_config_from_agent(discovery) end socket.close diff --git a/lib/instana/base.rb b/lib/instana/base.rb index 983ed97b..a1a9da3b 100644 --- a/lib/instana/base.rb +++ b/lib/instana/base.rb @@ -16,6 +16,15 @@ class << self attr_reader :serverless attr_accessor :tracer_provider + ## + # span_filtering_config + # + # Returns the span filtering configuration instance + # + def span_filtering_config + ::Instana::SpanFiltering.configuration + end + ## # setup # diff --git a/lib/instana/config.rb b/lib/instana/config.rb index 6521a8a4..8d41c2f2 100644 --- a/lib/instana/config.rb +++ b/lib/instana/config.rb @@ -1,6 +1,8 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2016 +require 'yaml' + module Instana class Config def initialize(logger: ::Instana.logger, agent_host: ENV['INSTANA_AGENT_HOST'], agent_port: ENV['INSTANA_AGENT_PORT']) @@ -43,8 +45,9 @@ def initialize(logger: ::Instana.logger, agent_host: ENV['INSTANA_AGENT_HOST'], # In Ruby, backtrace collection is very expensive so it's # (unfortunately) disabled by default. If you still want # backtraces, it can be enabled with this config option. - # ::Instana.config[:collect_backtraces] = true - @config[:collect_backtraces] = false + # @config[:back_trace][:stack_trace_level] = all + # @config[:back_trace] = { stack_trace_level: nil } + read_span_stack_config # By default, collected SQL will be sanitized to remove potentially sensitive bind params such as: # > SELECT "blocks".* FROM "blocks" WHERE "blocks"."name" = "Mr. Smith" @@ -86,6 +89,164 @@ def [](key) def []=(key, value) @config[key.to_sym] = value end + + # Read stack trace configuration from YAML file, environment variables, or use defaults + # Priority: YAML file > Environment variables > Agent discovery > Defaults + def read_span_stack_config + # Try to load from YAML file first + yaml_config = read_span_stack_config_from_yaml + + if yaml_config + @config[:back_trace] = yaml_config[:global] + @config[:back_trace_technologies] = yaml_config[:technologies] || {} + else + # Fall back to environment variables or defaults + read_span_stack_config_from_env + @config[:back_trace_technologies] = {} + end + end + + # Read configuration from agent discovery response + # This is called after agent discovery is complete + # @param discovery [Hash] The discovery response from the agent + def read_config_from_agent(discovery) + return unless discovery.is_a?(Hash) && discovery['tracing'] + + tracing_config = discovery['tracing'] + + # Read stack trace configuration from agent if not already set from YAML or env + read_span_stack_config_from_agent(tracing_config) if should_read_from_agent?(:back_trace) + # Read span filtering configuration from agent + ::Instana.span_filtering_config&.read_config_from_agent(discovery) + rescue => e + ::Instana.logger.warn("Failed to read configuration from agent: #{e.message}") + end + + # Read stack trace configuration from agent discovery + # @param tracing_config [Hash] The tracing configuration from discovery + def read_span_stack_config_from_agent(tracing_config) + return unless tracing_config['global'] + + global_config = tracing_config['global'] + stack_trace_level = global_config['stack-trace'] + stack_trace_length = global_config['stack-trace-length'] + + # Only update if at least one value is present + if stack_trace_level || stack_trace_length + @config[:back_trace] = { + stack_trace_level: stack_trace_level || 'error', + stack_trace_length: stack_trace_length ? stack_trace_length.to_i : 30, + config_source: 'agent' + } + end + + # Read technology-specific configurations + @config[:back_trace_technologies] = {} + tracing_config.each do |key, value| + next if key == 'global' || !value.is_a?(Hash) + + tech_stack_trace = value['stack-trace'] + tech_stack_trace_length = value['stack-trace-length'] + + next unless tech_stack_trace || tech_stack_trace_length + + @config[:back_trace_technologies][key.to_sym] = { + stack_trace_level: tech_stack_trace, + stack_trace_length: tech_stack_trace_length ? tech_stack_trace_length.to_i : nil + }.compact + end + end + + # Read stack trace configuration from YAML file + # Returns hash with :global and :technologies keys or nil if not found + def read_span_stack_config_from_yaml # rubocop:disable Metrics/CyclomaticComplexity + config_path = ENV['INSTANA_CONFIG_PATH'] + return nil unless config_path && File.exist?(config_path) + + begin + yaml_content = YAML.safe_load(File.read(config_path)) + + # Support both "tracing" and "com.instana.tracing" as top-level keys + tracing_config = yaml_content['tracing'] || yaml_content['com.instana.tracing'] + return nil unless tracing_config + + result = {} + + # Look for global stack trace configuration + if tracing_config['global'] + global_config = tracing_config['global'] + stack_trace_level = global_config['stack-trace'] + stack_trace_length = global_config['stack-trace-length'] + + if stack_trace_level || stack_trace_length + result[:global] = { + stack_trace_level: stack_trace_level || 'error', + stack_trace_length: stack_trace_length ? stack_trace_length.to_i : 30, + config_source: 'yaml' + } + end + end + + # Look for technology-specific configurations + technologies = {} + tracing_config.each do |key, value| + next if key == 'global' || !value.is_a?(Hash) + + tech_stack_trace = value['stack-trace'] + tech_stack_trace_length = value['stack-trace-length'] + + next unless tech_stack_trace || tech_stack_trace_length + + technologies[key.to_sym] = { + stack_trace_level: tech_stack_trace, + stack_trace_length: tech_stack_trace_length ? tech_stack_trace_length.to_i : nil + }.compact + end + + result[:technologies] = technologies unless technologies.empty? + + result.empty? ? nil : result + rescue => e + ::Instana.logger.warn("Failed to load stack trace configuration from YAML: #{e.message}") + nil + end + end + + # Read stack trace configuration from environment variables + def read_span_stack_config_from_env + stack_trace = ENV['INSTANA_STACK_TRACE'] + stack_trace_length = ENV['INSTANA_STACK_TRACE_LENGTH'] + config_source = stack_trace || stack_trace_length ? 'env' : 'default' + @config[:back_trace] = { + stack_trace_level: stack_trace || 'error', + stack_trace_length: stack_trace_length ? stack_trace_length.to_i : 30, + config_source: config_source + } + end + + # Check if we should read configuration from agent + # Returns true if config was not set from YAML or environment variables + def should_read_from_agent?(config_key) + return true unless @config[config_key] + + source = @config[config_key][:config_source] + source.nil? || source == 'default' + end + + # Get stack trace configuration for a specific technology + # Falls back to global configuration if technology-specific config is not found + # @param technology [Symbol] The technology name (e.g., :excon, :kafka, :activerecord) + # @return [Hash] Configuration hash with :stack_trace_level and :stack_trace_length + def get_stack_trace_config(technology) + tech_config = @config[:back_trace_technologies]&.[](technology) + global_config = @config[:back_trace] || {} + + { + stack_trace_level: tech_config&.[](:stack_trace_level) || global_config[:stack_trace_level] || 'error', + stack_trace_length: tech_config&.[](:stack_trace_length) || global_config[:stack_trace_length] || 30 + } + end + end end diff --git a/lib/instana/span_filtering/configuration.rb b/lib/instana/span_filtering/configuration.rb index 642a922c..a1b300ed 100644 --- a/lib/instana/span_filtering/configuration.rb +++ b/lib/instana/span_filtering/configuration.rb @@ -27,57 +27,21 @@ def initialize def load_configuration load_from_yaml load_from_env_vars unless rules_loaded? - load_from_agent unless rules_loaded? + # Agent configuration will be loaded after discovery via read_config_from_agent end - private - - # Load configuration from agent discovery response - def load_from_agent - # Try to get discovery value immediately first - discovery = ::Instana.agent&.delegate&.send(:discovery_value) - if discovery && discovery.is_a?(Hash) && !discovery.empty? - process_discovery_config(discovery) - return - end + # Read configuration from agent discovery response + # This is called from Config#read_config_from_agent after discovery is complete + # @param discovery [Hash] The discovery response from the agent + def read_config_from_agent(discovery) + return if rules_loaded? # Don't override if already loaded from YAML or env - # If not available, set up a timer task to periodically check for discovery - setup_discovery_timer + process_discovery_config(discovery) rescue => e - Instana.logger.warn("Failed to load span filtering configuration from agent: #{e.message}") + Instana.logger.warn("Failed to read span filtering configuration from agent: #{e.message}") end - # Set up a timer task to periodically check for discovery - def setup_discovery_timer - # Don't create a timer task if we're in a test environment - return if ENV.key?('INSTANA_TEST') - - # Create a timer task that checks for discovery every second - @discovery_timer = Concurrent::TimerTask.new(execution_interval: 1) do - check_discovery - end - - # Start the timer task - @discovery_timer.execute - end - - # Check if discovery is available and process it - def check_discovery - discovery = ::Instana.agent&.delegate.send(:discovery_value) - if discovery && discovery.is_a?(Hash) && !discovery.empty? - process_discovery_config(discovery) - - # Shutdown the timer task after successful processing - @discovery_timer.shutdown if @discovery_timer - - return true - end - - false - rescue => e - Instana.logger.warn("Error checking discovery in timer task: #{e.message}") - false - end + private # Process the discovery configuration def process_discovery_config(discovery) diff --git a/lib/instana/trace/span.rb b/lib/instana/trace/span.rb index c8d64ddf..c856f250 100644 --- a/lib/instana/trace/span.rb +++ b/lib/instana/trace/span.rb @@ -70,14 +70,18 @@ def initialize(name, parent_ctx = nil, _context = nil, parent_span = nil, _kind set_tags(attributes) ::Instana.processor.on_start(self) # Attach a backtrace to all exit spans - add_stack if ::Instana.config[:collect_backtraces] && exit_span? + add_stack if should_collect_stack_trace? end # Adds a backtrace to this span # # @param limit [Integer] Limit the backtrace to the top frames # - def add_stack(limit: 30, stack: Kernel.caller) + def add_stack(span_stack_config: nil, stack: Kernel.caller) + # Get technology-specific config if not provided + span_stack_config ||= current_span_stack_config + + limit = span_stack_config[:stack_trace_length] cleaner = ::Instana.config[:backtrace_cleaner] stack = cleaner.call(stack) if cleaner @@ -530,5 +534,20 @@ def add_event(_name, attributes: nil, timestamp: nil) # rubocop:disable Lint/Unu # # @return [void] def status=(status); end + + def should_collect_stack_trace? + return false unless exit_span? + + config = current_span_stack_config + config[:stack_trace_level] == "all" + end + + # Get the stack trace configuration for this span's technology + # Falls back to global configuration if technology-specific config is not found + # @return [Hash] Configuration hash with :stack_trace_level and :stack_trace_length + def current_span_stack_config + technology = @attributes[:n] + ::Instana.config.get_stack_trace_config(technology) + end end end diff --git a/test/config_test.rb b/test/config_test.rb index 5fcc85b1..c04b650a 100644 --- a/test/config_test.rb +++ b/test/config_test.rb @@ -3,7 +3,7 @@ require 'test_helper' -class ConfigTest < Minitest::Test +class ConfigTest < Minitest::Test # rubocop:disable Metrics/ClassLength def test_that_config_exists refute_nil ::Instana.config assert_instance_of(::Instana::Config, ::Instana.config) @@ -31,4 +31,652 @@ def test_custom_agent_port subject = Instana::Config.new(logger: Logger.new('/dev/null'), agent_port: 'abc') assert_equal 'abc', subject[:agent_port] end + + def test_read_span_stack_config_from_env_with_both_values + ENV['INSTANA_STACK_TRACE'] = 'all' + ENV['INSTANA_STACK_TRACE_LENGTH'] = '40' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 40, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE') + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_read_span_stack_config_from_env_with_error_level + ENV['INSTANA_STACK_TRACE'] = 'error' + ENV['INSTANA_STACK_TRACE_LENGTH'] = '30' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE') + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_read_span_stack_config_from_env_with_none_level + ENV['INSTANA_STACK_TRACE'] = 'none' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal 'none', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE') + end + + def test_read_span_stack_config_from_env_with_only_stack_trace_length + ENV['INSTANA_STACK_TRACE_LENGTH'] = '20' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal "error", subject[:back_trace][:stack_trace_level] + assert_equal 20, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_read_span_stack_config_from_env_with_no_env_vars + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal "error", subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'default', subject[:back_trace][:config_source] + end + + def test_read_span_stack_config_from_env_converts_length_to_integer + ENV['INSTANA_STACK_TRACE_LENGTH'] = '25' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal 25, subject[:back_trace][:stack_trace_length] + assert_instance_of Integer, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_read_span_stack_config_from_env_with_zero_length + ENV['INSTANA_STACK_TRACE_LENGTH'] = '0' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_span_stack_config_from_env + + assert_equal 0, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + # Tests for YAML configuration reading + + def test_read_span_stack_config_from_yaml_with_both_values + yaml_content = <<~YAML + tracing: + global: + stack-trace: all + stack-trace-length: 25 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 25, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_from_yaml_with_error_level + yaml_content = <<~YAML + tracing: + global: + stack-trace: error + stack-trace-length: 15 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 15, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_from_yaml_with_none_level + yaml_content = <<~YAML + tracing: + global: + stack-trace: none + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'none', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_from_yaml_with_only_length + yaml_content = <<~YAML + tracing: + global: + stack-trace-length: 10 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 10, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_from_yaml_with_com_instana_tracing + yaml_content = <<~YAML + com.instana.tracing: + global: + stack-trace: all + stack-trace-length: 20 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 20, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_yaml_missing_global_section + yaml_content = <<~YAML + tracing: + filter: + deactivate: false + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Should fall back to defaults + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'default', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_yaml_takes_precedence_over_env + yaml_content = <<~YAML + tracing: + global: + stack-trace: all + stack-trace-length: 50 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + ENV['INSTANA_STACK_TRACE'] = 'none' + ENV['INSTANA_STACK_TRACE_LENGTH'] = '5' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # YAML should take precedence + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 50, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + ENV.delete('INSTANA_STACK_TRACE') + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_read_span_stack_config_invalid_yaml_falls_back_to_env + File.write('test_stack_config.yaml', "invalid: yaml: content: - [") + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + ENV['INSTANA_STACK_TRACE'] = 'all' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Should fall back to env vars + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + ENV.delete('INSTANA_STACK_TRACE') + end + + # Tests for reading configuration from agent discovery + + def test_read_config_from_agent_with_stack_trace_config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 100 + } + } + } + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_config_from_agent(discovery) + + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 100, subject[:back_trace][:stack_trace_length] + assert_equal 'agent', subject[:back_trace][:config_source] + end + + def test_read_config_from_agent_with_only_stack_trace_level + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'none' + } + } + } + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_config_from_agent(discovery) + + assert_equal 'none', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'agent', subject[:back_trace][:config_source] + end + + def test_read_config_from_agent_with_only_stack_trace_length + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace-length' => 75 + } + } + } + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_config_from_agent(discovery) + + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 75, subject[:back_trace][:stack_trace_length] + assert_equal 'agent', subject[:back_trace][:config_source] + end + + def test_read_config_from_agent_without_tracing_config + discovery = { + 'pid' => 12345, + 'agentUuid' => 'test-uuid' + } + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + original_config = subject[:back_trace].dup + subject.read_config_from_agent(discovery) + + # Config should remain unchanged + assert_equal original_config, subject[:back_trace] + end + + def test_read_config_from_agent_with_empty_discovery + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + original_config = subject[:back_trace].dup + subject.read_config_from_agent({}) + + # Config should remain unchanged + assert_equal original_config, subject[:back_trace] + end + + def test_read_config_from_agent_with_nil_discovery + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + original_config = subject[:back_trace].dup + subject.read_config_from_agent(nil) + + # Config should remain unchanged + assert_equal original_config, subject[:back_trace] + end + + # Tests for configuration priority: YAML > Env > Agent > Default + + def test_priority_yaml_over_agent + yaml_content = <<~YAML + tracing: + global: + stack-trace: none + stack-trace-length: 10 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Try to override with agent config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 100 + } + } + } + subject.read_config_from_agent(discovery) + + # YAML should take precedence + assert_equal 'none', subject[:back_trace][:stack_trace_level] + assert_equal 10, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_priority_env_over_agent + ENV['INSTANA_STACK_TRACE'] = 'error' + ENV['INSTANA_STACK_TRACE_LENGTH'] = '20' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Try to override with agent config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 100 + } + } + } + subject.read_config_from_agent(discovery) + + # Env should take precedence + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 20, subject[:back_trace][:stack_trace_length] + assert_equal 'env', subject[:back_trace][:config_source] + ensure + ENV.delete('INSTANA_STACK_TRACE') + ENV.delete('INSTANA_STACK_TRACE_LENGTH') + end + + def test_priority_agent_over_default + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Verify default config + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'default', subject[:back_trace][:config_source] + + # Override with agent config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 50 + } + } + } + subject.read_config_from_agent(discovery) + + # Agent should override default + assert_equal 'all', subject[:back_trace][:stack_trace_level] + assert_equal 50, subject[:back_trace][:stack_trace_length] + assert_equal 'agent', subject[:back_trace][:config_source] + end + + def test_should_read_from_agent_returns_true_for_default_config + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + assert subject.send(:should_read_from_agent?, :back_trace) + end + + def test_should_read_from_agent_returns_false_for_yaml_config + yaml_content = <<~YAML + tracing: + global: + stack-trace: all + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + refute subject.send(:should_read_from_agent?, :back_trace) + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_should_read_from_agent_returns_false_for_env_config + ENV['INSTANA_STACK_TRACE'] = 'all' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + refute subject.send(:should_read_from_agent?, :back_trace) + ensure + ENV.delete('INSTANA_STACK_TRACE') + end + + # Tests for technology-specific stack trace configuration + + def test_read_span_stack_config_from_yaml_with_technology_specific_config + yaml_content = <<~YAML + com.instana.tracing: + global: + stack-trace: error + stack-trace-length: 25 + + kafka: + stack-trace: all + + redis: + stack-trace: all + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Check global config + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 25, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + + # Check technology-specific configs + assert_equal 'all', subject[:back_trace_technologies][:kafka][:stack_trace_level] + assert_equal 'all', subject[:back_trace_technologies][:redis][:stack_trace_level] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_span_stack_config_from_yaml_with_technology_specific_length + yaml_content = <<~YAML + tracing: + global: + stack-trace: error + stack-trace-length: 30 + + kafka: + stack-trace: all + stack-trace-length: 50 + + redis: + stack-trace-length: 10 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Check kafka config + kafka_config = subject[:back_trace_technologies][:kafka] + assert_equal 'all', kafka_config[:stack_trace_level] + assert_equal 50, kafka_config[:stack_trace_length] + + # Check redis config (only length specified) + redis_config = subject[:back_trace_technologies][:redis] + assert_nil redis_config[:stack_trace_level] + assert_equal 10, redis_config[:stack_trace_length] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_get_stack_trace_config_for_technology + yaml_content = <<~YAML + tracing: + global: + stack-trace: error + stack-trace-length: 30 + + kafka: + stack-trace: all + stack-trace-length: 50 + + redis: + stack-trace: all + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Test kafka - should use technology-specific config + kafka_config = subject.get_stack_trace_config(:kafka) + assert_equal 'all', kafka_config[:stack_trace_level] + assert_equal 50, kafka_config[:stack_trace_length] + + # Test redis - should use technology-specific level, global length + redis_config = subject.get_stack_trace_config(:redis) + assert_equal 'all', redis_config[:stack_trace_level] + assert_equal 30, redis_config[:stack_trace_length] + + # Test excon - should fall back to global config + excon_config = subject.get_stack_trace_config(:excon) + assert_equal 'error', excon_config[:stack_trace_level] + assert_equal 30, excon_config[:stack_trace_length] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end + + def test_read_config_from_agent_with_technology_specific_config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'error', + 'stack-trace-length' => 30 + }, + 'kafka' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 100 + }, + 'redis' => { + 'stack-trace' => 'all' + } + } + } + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + subject.read_config_from_agent(discovery) + + # Check global config + assert_equal 'error', subject[:back_trace][:stack_trace_level] + assert_equal 30, subject[:back_trace][:stack_trace_length] + assert_equal 'agent', subject[:back_trace][:config_source] + + # Check technology-specific configs + kafka_config = subject[:back_trace_technologies][:kafka] + assert_equal 'all', kafka_config[:stack_trace_level] + assert_equal 100, kafka_config[:stack_trace_length] + + redis_config = subject[:back_trace_technologies][:redis] + assert_equal 'all', redis_config[:stack_trace_level] + assert_nil redis_config[:stack_trace_length] + end + + def test_yaml_technology_config_not_overridden_by_agent + yaml_content = <<~YAML + tracing: + global: + stack-trace: none + stack-trace-length: 10 + + kafka: + stack-trace: error + stack-trace-length: 20 + YAML + + File.write('test_stack_config.yaml', yaml_content) + ENV['INSTANA_CONFIG_PATH'] = 'test_stack_config.yaml' + + subject = Instana::Config.new(logger: Logger.new('/dev/null')) + + # Try to override with agent config + discovery = { + 'tracing' => { + 'global' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 100 + }, + 'kafka' => { + 'stack-trace' => 'all', + 'stack-trace-length' => 200 + } + } + } + subject.read_config_from_agent(discovery) + + # YAML should take precedence for both global and technology-specific + assert_equal 'none', subject[:back_trace][:stack_trace_level] + assert_equal 10, subject[:back_trace][:stack_trace_length] + assert_equal 'yaml', subject[:back_trace][:config_source] + + kafka_config = subject[:back_trace_technologies][:kafka] + assert_equal 'error', kafka_config[:stack_trace_level] + assert_equal 20, kafka_config[:stack_trace_length] + ensure + File.unlink('test_stack_config.yaml') if File.exist?('test_stack_config.yaml') + ENV.delete('INSTANA_CONFIG_PATH') + end end diff --git a/test/instrumentation/dalli_test.rb b/test/instrumentation/dalli_test.rb index 6658038b..9f39fe2c 100644 --- a/test/instrumentation/dalli_test.rb +++ b/test/instrumentation/dalli_test.rb @@ -188,7 +188,7 @@ def test_incr clear_all! result = nil - @dc.set(:counter, 0, nil, :raw => true) + @dc.set(:counter, '0', nil, :raw => true) ::Instana.tracer.in_span(:dalli_test) do result = @dc.incr(:counter, 1, nil, 0) end @@ -222,7 +222,7 @@ def test_decr clear_all! result = nil - @dc.set(:counter, 0, nil, :raw => true) + @dc.set(:counter, '0', nil, :raw => true) ::Instana.tracer.in_span(:dalli_test) do result = @dc.decr(:counter, 1, nil, 0) end diff --git a/test/instrumentation/rack_test.rb b/test/instrumentation/rack_test.rb index adc68fd2..47b4ae10 100644 --- a/test/instrumentation/rack_test.rb +++ b/test/instrumentation/rack_test.rb @@ -58,7 +58,7 @@ def app def test_basic_get clear_all! - ::Instana.config[:collect_backtraces] = true + Instana.config[:back_trace][:stack_trace_level] = "all" get '/mrlobster' assert last_response.ok? @@ -94,7 +94,7 @@ def test_basic_get assert !rack_span.key?(:stack) # Restore to default - ::Instana.config[:collect_backtraces] = false + ::Instana.config[:back_trace][:stack_trace_level] = 'none' end def test_basic_get_with_custom_service_name @@ -258,7 +258,7 @@ def test_that_url_params_not_logged def test_custom_headers_capture clear_all! - ::Instana.config[:collect_backtraces] = true + ::Instana.config[:back_trace][:stack_trace_level] = 'all' ::Instana.agent.define_singleton_method(:extra_headers) { %w(X-Capture-This X-Capture-That) } get '/mrlobster', {}, { "HTTP_X_CAPTURE_THIS" => "ThereYouGo" } @@ -278,7 +278,7 @@ def test_custom_headers_capture assert !rack_span.key?(:stack) # Restore to default - ::Instana.config[:collect_backtraces] = false + ::Instana.config[:back_trace][:stack_trace_level] = 'none' ::Instana.agent.singleton_class.send(:remove_method, :extra_headers) end diff --git a/test/span_filtering/configuration_test.rb b/test/span_filtering/configuration_test.rb index cbda4fe2..4b465618 100644 --- a/test/span_filtering/configuration_test.rb +++ b/test/span_filtering/configuration_test.rb @@ -202,7 +202,7 @@ def test_load_from_both_yaml_and_env_vars end def test_load_from_agent_discovery - # Create a mock agent with discovery value + # Create a discovery value discovery_value = { 'tracing' => { 'filter' => { @@ -235,17 +235,12 @@ def test_load_from_agent_discovery } } - # Create a mock agent - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - # Replace the global agent with our mock - ::Instana.instance_variable_set(:@agent, mock_agent) - - # Create a new configuration that should load from our mock agent + # Create a new configuration config = Instana::SpanFiltering::Configuration.new + # Simulate loading from agent after discovery + config.read_config_from_agent(discovery_value) + # Verify the configuration was loaded correctly assert_equal 1, config.include_rules.size assert_equal 'include-http', config.include_rules.first.name @@ -253,12 +248,10 @@ def test_load_from_agent_discovery assert_equal 1, config.exclude_rules.size assert_equal 'exclude-redis', config.exclude_rules.first.name assert config.exclude_rules.first.suppression - - mock_agent.verify end def test_load_from_agent_with_deactivation - # Create a mock agent with discovery value that has deactivation flag + # Create a discovery value that has deactivation flag discovery_value = { 'tracing' => { 'filter' => { @@ -267,43 +260,29 @@ def test_load_from_agent_with_deactivation } } - # Create a mock agent - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - # Replace the global agent with our mock - ::Instana.instance_variable_set(:@agent, mock_agent) - - # Create a new configuration that should load from our mock agent + # Create a new configuration config = Instana::SpanFiltering::Configuration.new + # Simulate loading from agent after discovery + config.read_config_from_agent(discovery_value) + # Verify the configuration was loaded correctly assert config.deactivated assert_empty config.include_rules assert_empty config.exclude_rules - - mock_agent.verify end def test_load_from_agent_with_empty_discovery - # Create a mock agent with empty discovery value - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, {}) - - # Replace the global agent with our mock - ::Instana.instance_variable_set(:@agent, mock_agent) - - # Create a new configuration that should try to load from our mock agent + # Create a new configuration config = Instana::SpanFiltering::Configuration.new + # Simulate loading from agent with empty discovery + config.read_config_from_agent({}) + # Verify the configuration was not loaded (empty) refute config.deactivated assert_empty config.include_rules assert_empty config.exclude_rules - - mock_agent.verify end def test_load_from_agent_with_nil_agent @@ -339,46 +318,9 @@ def mock_agent.discovery_value assert_empty config.exclude_rules end - def test_load_from_agent_with_timer_task # rubocop:disable Metrics/MethodLength - # Save original INSTANA_TEST value - original_test_env = ENV['INSTANA_TEST'] - ENV.delete('INSTANA_TEST') # Temporarily remove INSTANA_TEST to allow timer task creation - - # Mock the Concurrent::TimerTask class - original_timer_task = Concurrent::TimerTask - Concurrent.send(:remove_const, :TimerTask) - - # Create a custom timer task class that immediately executes the block - Concurrent.const_set(:TimerTask, Class.new do - def initialize(*args, &block) - @block = block - @running = false - @args = args - end - - def execute - @running = true - # Immediately execute the block when execute is called - @block.call - true - end - - def shutdown - @running = false - end - - def running? - @running - end - end) - - # Create a mock agent with nil discovery initially, then with real discovery later - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, nil) - - # We need to set up the mock to return real discovery value on second call - # This will be called by the timer task + def test_load_from_agent_with_timer_task + # This test is no longer relevant as we removed the timer task dependency + # Configuration is now loaded via read_config_from_agent after discovery discovery_value = { 'tracing' => { 'filter' => { @@ -398,34 +340,15 @@ def running? } } - # Set up the mock to return real discovery value on second call - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - # Replace the global agent with our mock - ::Instana.instance_variable_set(:@agent, mock_agent) - - # Create a new configuration that should set up a timer task + # Create a new configuration config = Instana::SpanFiltering::Configuration.new - # Verify the configuration was loaded by the timer task + # Simulate loading from agent after discovery (replaces timer task behavior) + config.read_config_from_agent(discovery_value) + + # Verify the configuration was loaded assert_equal 1, config.include_rules.size assert_equal 'include-http', config.include_rules.first.name - - mock_agent.verify - ensure - # Restore the original TimerTask class - if Concurrent.const_defined?(:TimerTask) - Concurrent.send(:remove_const, :TimerTask) - Concurrent.const_set(:TimerTask, original_timer_task) - end - - # Restore original INSTANA_TEST value - if original_test_env - ENV['INSTANA_TEST'] = original_test_env - else - ENV.delete('INSTANA_TEST') - end end end @@ -550,63 +473,45 @@ def test_redis_disabled_via_env_var_multiple end def test_redis_disabled_via_agent_discovery_string_format - # Create a mock agent with discovery value using string format + # Create a discovery value using string format discovery_value = { 'tracing' => { - 'disable' => [{'redis' => true}] + 'disable' => ['redis'] } } - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - ::Instana.instance_variable_set(:@agent, mock_agent) - - Instana::SpanFiltering::Configuration.new + config = Instana::SpanFiltering::Configuration.new + config.read_config_from_agent(discovery_value) refute ::Instana.config[:redis][:enabled], "Redis should be disabled via agent discovery string format" - mock_agent.verify end def test_redis_disabled_via_agent_discovery_hash_format - # Create a mock agent with discovery value using hash format + # Create a discovery value using hash format discovery_value = { 'tracing' => { 'disable' => [{'redis' => true}] } } - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - ::Instana.instance_variable_set(:@agent, mock_agent) - - Instana::SpanFiltering::Configuration.new + config = Instana::SpanFiltering::Configuration.new + config.read_config_from_agent(discovery_value) refute ::Instana.config[:redis][:enabled], "Redis should be disabled via agent discovery hash format" - mock_agent.verify end def test_redis_disabled_via_agent_discovery_databases - # Create a mock agent with discovery value disabling databases category + # Create a discovery value disabling databases category discovery_value = { 'tracing' => { 'disable' => [{'databases' => true}] } } - mock_agent = Minitest::Mock.new - mock_agent.expect(:delegate, mock_agent) - mock_agent.expect(:discovery_value, discovery_value) - - ::Instana.instance_variable_set(:@agent, mock_agent) - - Instana::SpanFiltering::Configuration.new + config = Instana::SpanFiltering::Configuration.new + config.read_config_from_agent(discovery_value) refute ::Instana.config[:redis][:enabled], "Redis should be disabled when databases category is disabled via agent discovery" - mock_agent.verify end def test_yaml_config_takes_precedence_over_agent_discovery diff --git a/test/trace/span_test.rb b/test/trace/span_test.rb index e3d21593..48b6fc62 100644 --- a/test/trace/span_test.rb +++ b/test/trace/span_test.rb @@ -56,15 +56,15 @@ def test_span_from_contetx_invalid end def test_span_collect_backtraces - Instana.config[:collect_backtraces] = true + Instana.config[:back_trace][:stack_trace_level] = "all" span = Instana::Span.new(:excon) assert span[:stack] ensure - Instana.config[:collect_backtraces] = false + Instana.config[:back_trace][:stack_trace_level] = nil end def test_span_backtrace_cleaner - Instana.config[:collect_backtraces] = true + ::Instana.config[:back_trace][:stack_trace_level] = "all" Instana.config[:backtrace_cleaner] = ->(trace) { trace.filter { |line| line.include?("lib/instana") } } span = Instana::Span.new(:excon) @@ -72,7 +72,7 @@ def test_span_backtrace_cleaner assert_equal 1, span[:stack].size ensure Instana.config[:backtrace_cleaner] = nil - Instana.config[:collect_backtraces] = false + Instana.config[:back_trace][:stack_trace_level] = nil end def test_span_stack_over_limit @@ -84,7 +84,7 @@ def inner(depth = 50, &blk) # rubocop:disable Lint/NestedMethodDefinition inner do span = Instana::Span.new(:excon) - span.add_stack(limit: 500) + span.add_stack(span_stack_config: { stack_trace_length: 500}) assert_equal 40, span[:stack].length end end @@ -176,4 +176,230 @@ def test_no_custom_service_name_set span = Instana::Span.new(:excon) assert_nil(span[:data][:service]) end + + # Tests for stack_trace_level configuration + + def test_stack_trace_level_all_collects_for_all_spans + Instana.config[:back_trace][:stack_trace_level] = "all" + span = Instana::Span.new(:excon) + + assert span[:stack], "Stack trace should be collected for all spans when level is 'all'" + assert span[:stack].is_a?(Array), "Stack trace should be an array" + assert span[:stack].length.positive?, "Stack trace should not be empty" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + def test_stack_trace_level_error_does_not_collect_for_normal_spans + Instana.config[:back_trace][:stack_trace_level] = "error" + span = Instana::Span.new(:excon) + + assert_nil span[:stack], "Stack trace should not be collected for normal spans when level is 'error'" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + def test_stack_trace_level_error_collects_for_erroneous_spans + Instana.config[:back_trace][:stack_trace_level] = "error" + span = Instana::Span.new(:excon) + + # Span should not have stack trace at creation with level 'error' + assert_nil span[:stack], "Stack trace should not be collected at span creation when level is 'error'" + + # Record an exception to make it an erroneous span + # Need to raise the exception to populate its backtrace + begin + raise StandardError, "Test error" + rescue StandardError => e + span.record_exception(e) + end + + # Stack trace from the exception backtrace should be collected + assert span[:stack], "Stack trace from exception should be collected" + assert span[:stack].is_a?(Array), "Stack trace should be an array" + assert span[:stack].length.positive?, "Stack trace should not be empty" + assert_equal 1, span[:ec], "Error count should be 1" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + def test_stack_trace_level_none_does_not_collect_for_normal_spans + Instana.config[:back_trace][:stack_trace_level] = "none" + span = Instana::Span.new(:excon) + + assert_nil span[:stack], "Stack trace should not be collected when level is 'none'" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + def test_stack_trace_level_none_does_not_collect_for_erroneous_spans + Instana.config[:back_trace][:stack_trace_level] = "none" + span = Instana::Span.new(:excon) + + # Span should not have stack trace at creation with level 'none' + assert_nil span[:stack], "Stack trace should not be collected at span creation when level is 'none'" + + # Record an exception - need to raise it to populate backtrace + begin + raise StandardError, "Test error" + rescue StandardError => e + span.record_exception(e) + end + + # NOTE: record_exception always collects the exception's backtrace regardless of stack_trace_level + # This is by design - the stack_trace_level only controls automatic collection at span creation + assert span[:stack], "Stack trace from exception backtrace is always collected by record_exception" + assert_equal 1, span[:ec], "Error count should be 1" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + # Tests for stack_trace_length configuration + + def test_stack_trace_length_limits_frames + Instana.config[:back_trace][:stack_trace_level] = "all" + Instana.config[:back_trace][:stack_trace_length] = 5 + + span = Instana::Span.new(:excon) + + assert span[:stack], "Stack trace should be collected" + assert span[:stack].length <= 5, "Stack trace should be limited to 5 frames" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + def test_stack_trace_length_with_different_values + Instana.config[:back_trace][:stack_trace_level] = "all" + + # Test with length 10 + Instana.config[:back_trace][:stack_trace_length] = 10 + span1 = Instana::Span.new(:excon) + assert span1[:stack].length <= 10, "Stack trace should be limited to 10 frames" + + # Test with length 20 + Instana.config[:back_trace][:stack_trace_length] = 20 + span2 = Instana::Span.new(:excon) + assert span2[:stack].length <= 20, "Stack trace should be limited to 20 frames" + + # Test with length 1 + Instana.config[:back_trace][:stack_trace_length] = 1 + span3 = Instana::Span.new(:excon) + assert span3[:stack].length <= 1, "Stack trace should be limited to 1 frame" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + def test_stack_trace_length_zero_collects_no_frames + Instana.config[:back_trace][:stack_trace_level] = "all" + Instana.config[:back_trace][:stack_trace_length] = 0 + + span = Instana::Span.new(:excon) + + # With length 0, stack should either be nil or empty array + assert(span[:stack].nil? || span[:stack].empty?, "Stack trace should be empty with length 0") + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + # Combined tests for stack_trace_level and stack_trace_length + + def test_stack_trace_all_with_custom_length + Instana.config[:back_trace][:stack_trace_level] = "all" + Instana.config[:back_trace][:stack_trace_length] = 15 + + span = Instana::Span.new(:excon) + + assert span[:stack], "Stack trace should be collected with level 'all'" + assert span[:stack].length <= 15, "Stack trace should respect custom length of 15" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + def test_stack_trace_error_with_custom_length_on_error + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 8 + + span = Instana::Span.new(:excon) + + # No stack at creation with level 'error' + assert_nil span[:stack], "Stack trace should not be collected at span creation when level is 'error'" + + # Raise exception to populate backtrace + begin + raise StandardError, "Test error" + rescue StandardError => e + span.record_exception(e) + end + + # Stack trace from exception should be collected and respect length limit + assert span[:stack], "Stack trace from exception should be collected" + assert span[:stack].length <= 8, "Stack trace should respect custom length of 8" + assert_equal 1, span[:ec], "Error count should be 1" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + def test_stack_trace_none_ignores_length_setting + Instana.config[:back_trace][:stack_trace_level] = "none" + Instana.config[:back_trace][:stack_trace_length] = 100 + + span = Instana::Span.new(:excon) + + assert_nil span[:stack], "Stack trace should not be collected when level is 'none', regardless of length" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + Instana.config[:back_trace][:stack_trace_length] = 30 + end + + # Test for non-exit spans + + def test_stack_trace_not_collected_for_non_exit_spans + Instana.config[:back_trace][:stack_trace_level] = "all" + + # Create a non-exit span (sdk/custom span) + span = Instana::Span.new(:sdk) + + # Non-exit spans should not collect stack traces automatically + assert_nil span[:stack], "Stack trace should not be collected for non-exit spans" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end + + def test_stack_trace_for_multiple_errors_with_error_level + Instana.config[:back_trace][:stack_trace_level] = "error" + + span = Instana::Span.new(:excon) + + # No stack at creation with level 'error' + assert_nil span[:stack], "Stack trace should not be collected at span creation when level is 'error'" + + # First error - raise to populate backtrace + begin + raise StandardError, "First error" + rescue StandardError => e + span.record_exception(e) + end + + assert span[:stack], "Stack trace from first exception should be collected" + first_stack = span[:stack].dup + + # Second error - raise to populate backtrace + begin + raise StandardError, "Second error" + rescue StandardError => e + span.record_exception(e) + end + + assert span[:stack], "Stack trace from second exception should be collected" + assert_equal 2, span[:ec], "Error count should be 2" + # The stack from the second error should replace the first + refute_equal first_stack, span[:stack], "Stack trace should be updated with second error" + ensure + Instana.config[:back_trace][:stack_trace_level] = "error" + end end