lib/elastic_apm/config.rb in elastic-apm-2.9.1 vs lib/elastic_apm/config.rb in elastic-apm-2.10.0

- old
+ new

@@ -3,252 +3,115 @@ require 'logger' require 'yaml' require 'erb' require 'elastic_apm/util/prefixed_logger' + +require 'elastic_apm/config/options' require 'elastic_apm/config/duration' -require 'elastic_apm/config/size' +require 'elastic_apm/config/bytes' +require 'elastic_apm/config/regexp_list' module ElasticAPM - class ConfigError < StandardError; end - # rubocop:disable Metrics/ClassLength # @api private class Config - DEFAULTS = { - config_file: 'config/elastic_apm.yml', + extend Options - server_url: 'http://localhost:8200', - - active: true, - api_buffer_size: 256, - api_request_size: '750kb', - api_request_time: '10s', - capture_body: 'off', - capture_headers: true, - capture_env: true, - current_user_email_method: :email, - current_user_id_method: :id, - current_user_username_method: :username, - custom_key_filters: [], - default_tags: {}, - disable_send: false, - disable_start_message: false, - disabled_spies: %w[json], - environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'], - filter_exception_types: [], - http_compression: true, - ignore_url_patterns: [], - instrument: true, - instrumented_rake_tasks: [], - log_level: Logger::INFO, - log_path: nil, - metrics_interval: '30s', - pool_size: 1, - source_lines_error_app_frames: 5, - source_lines_error_library_frames: 0, - source_lines_span_app_frames: 5, - source_lines_span_library_frames: 0, - span_frames_min_duration: '5ms', - stack_trace_limit: 999_999, - transaction_max_spans: 500, - transaction_sample_rate: 1.0, - verify_server_cert: true, - - view_paths: [], - root_path: Dir.pwd - }.freeze - - ENV_TO_KEY = { - 'ELASTIC_APM_SERVER_URL' => 'server_url', - 'ELASTIC_APM_SECRET_TOKEN' => 'secret_token', - - 'ELASTIC_APM_ACTIVE' => [:bool, 'active'], - 'ELASTIC_APM_API_BUFFER_SIZE' => [:int, 'api_buffer_size'], - 'ELASTIC_APM_API_REQUEST_SIZE' => [:int, 'api_request_size'], - 'ELASTIC_APM_API_REQUEST_TIME' => 'api_request_time', - 'ELASTIC_APM_CAPTURE_BODY' => 'capture_body', - 'ELASTIC_APM_CAPTURE_HEADERS' => [:bool, 'capture_headers'], - 'ELASTIC_APM_CAPTURE_ENV' => [:bool, 'capture_env'], - 'ELASTIC_APM_CONFIG_FILE' => 'config_file', - 'ELASTIC_APM_CUSTOM_KEY_FILTERS' => [:list, 'custom_key_filters'], - 'ELASTIC_APM_DEFAULT_TAGS' => [:dict, 'default_tags'], - 'ELASTIC_APM_DISABLED_SPIES' => [:list, 'disabled_spies'], - 'ELASTIC_APM_DISABLE_SEND' => [:bool, 'disable_send'], - 'ELASTIC_APM_DISABLE_START_MESSAGE' => [:bool, 'disable_start_message'], - 'ELASTIC_APM_ENVIRONMENT' => 'environment', - 'ELASTIC_APM_FRAMEWORK_NAME' => 'framework_name', - 'ELASTIC_APM_FRAMEWORK_VERSION' => 'framework_version', - 'ELASTIC_APM_HOSTNAME' => 'hostname', - 'ELASTIC_APM_IGNORE_URL_PATTERNS' => [:list, 'ignore_url_patterns'], - 'ELASTIC_APM_INSTRUMENT' => [:bool, 'instrument'], - 'ELASTIC_APM_INSTRUMENTED_RAKE_TASKS' => - [:list, 'instrumented_rake_tasks'], - 'ELASTIC_APM_LOG_LEVEL' => [:int, 'log_level'], - 'ELASTIC_APM_LOG_PATH' => 'log_path', - 'ELASTIC_APM_METRICS_INTERVAL' => 'metrics_interval', - 'ELASTIC_APM_PROXY_ADDRESS' => 'proxy_address', - 'ELASTIC_APM_PROXY_HEADERS' => [:dict, 'proxy_headers'], - 'ELASTIC_APM_PROXY_PASSWORD' => 'proxy_password', - 'ELASTIC_APM_PROXY_PORT' => [:int, 'proxy_port'], - 'ELASTIC_APM_PROXY_USERNAME' => 'proxy_username', - 'ELASTIC_APM_POOL_SIZE' => [:int, 'pool_size'], - 'ELASTIC_APM_SERVER_CA_CERT' => 'server_ca_cert', - 'ELASTIC_APM_SERVICE_NAME' => 'service_name', - 'ELASTIC_APM_SERVICE_VERSION' => 'service_version', - 'ELASTIC_APM_SOURCE_LINES_ERROR_APP_FRAMES' => - [:int, 'source_lines_error_app_frames'], - 'ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES' => - [:int, 'source_lines_error_library_frames'], - 'ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES' => - [:int, 'source_lines_span_app_frames'], - 'ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES' => - [:int, 'source_lines_span_library_frames'], - 'ELASTIC_APM_SPAN_FRAMES_MIN_DURATION' => 'span_frames_min_duration', - 'ELASTIC_APM_STACK_TRACE_LIMIT' => [:int, 'stack_trace_limit'], - 'ELASTIC_APM_TRANSACTION_MAX_SPANS' => [:int, 'transaction_max_spans'], - 'ELASTIC_APM_TRANSACTION_SAMPLE_RATE' => - [:float, 'transaction_sample_rate'], - 'ELASTIC_APM_VERIFY_SERVER_CERT' => [:bool, 'verify_server_cert'] - }.freeze - - DURATION_KEYS = %i[ - api_request_time - span_frames_min_duration - metrics_interval + DEPRECATED_OPTIONS = %i[ + compression_level= + compression_minimum_size= + debug_http= + debug_transactions= + flush_interval= + http_open_timeout= + http_read_timeout= + enabled_environments= + disable_environment_warning= ].freeze - DURATION_DEFAULT_UNITS = { # default is 's' - span_frames_min_duration: 'ms' - }.freeze - SIZE_KEYS = %i[api_request_size].freeze - SIZE_DEFAULT_UNITS = { api_request_size: 'kb' }.freeze + # rubocop:disable Metrics/LineLength, Layout/ExtraSpacing + option :config_file, default: 'config/elastic_apm.yml' + option :server_url, default: 'http://localhost:8200' + option :secret_token + option :active, type: :bool, default: true + option :api_buffer_size, type: :int, default: 256 + option :api_request_size, type: :bytes, default: '750kb', converter: Bytes.new + option :api_request_time, type: :float, default: '10s', converter: Duration.new + option :capture_body, type: :string, default: 'off' + option :capture_headers, type: :bool, default: true + option :capture_env, type: :bool, default: true + option :central_config, type: :bool, default: true + option :current_user_email_method, type: :string, default: 'email' + option :current_user_id_method, type: :string, default: 'id' + option :current_user_username_method, type: :string, default: 'username' + option :custom_key_filters, type: :list, default: [], converter: RegexpList.new + option :default_tags, type: :dict, default: {} + option :disable_send, type: :bool, default: false + option :disable_start_message, type: :bool, default: false + option :disabled_spies, type: :list, default: %w[json] + option :environment, type: :string, default: ENV['RAILS_ENV'] || ENV['RACK_ENV'] + option :framework_name, type: :string + option :framework_version, type: :string + option :filter_exception_types, type: :list, default: [] + option :global_labels, type: :dict + option :hostname, type: :string + option :http_compression, type: :bool, default: true + option :ignore_url_patterns, type: :list, default: [], converter: RegexpList.new + option :instrument, type: :bool, default: true + option :instrumented_rake_tasks, type: :list, default: [] + option :log_level, type: :int, default: Logger::INFO + option :log_path, type: :string + option :metrics_interval, type: :int, default: '30s', converter: Duration.new + option :pool_size, type: :int, default: 1 + option :proxy_address, type: :string + option :proxy_headers, type: :dict + option :proxy_password, type: :string + option :proxy_port, type: :int + option :proxy_username, type: :string + option :server_ca_cert, type: :string + option :service_name, type: :string + option :service_version, type: :string + option :source_lines_error_app_frames, type: :int, default: 5 + option :source_lines_error_library_frames, type: :int, default: 0 + option :source_lines_span_app_frames, type: :int, default: 5 + option :source_lines_span_library_frames, type: :int, default: 0 + option :span_frames_min_duration, type: :float, default: '5ms', converter: Duration.new(default_unit: 'ms') + option :stack_trace_limit, type: :int, default: 999_999 + option :transaction_max_spans, type: :int, default: 500 + option :transaction_sample_rate, type: :float, default: 1.0 + option :verify_server_cert, type: :bool, default: true + # rubocop:enable Metrics/LineLength, Layout/ExtraSpacing + def initialize(options = {}) - set_defaults + @options = load_schema - set_from_args(options) - set_from_config_file - set_from_env + custom_logger = options.delete(:logger) - normalize_durations - normalize_sizes + assign(options) + assign(load_config_file) + assign(load_env) yield self if block_given? - build_logger if logger.nil? + @logger = custom_logger || build_logger + + @__view_paths = [] + @__root_path = Dir.pwd end - attr_accessor :config_file - - attr_accessor :server_url - attr_accessor :secret_token - - attr_accessor :active - attr_accessor :api_buffer_size - attr_accessor :api_request_size - attr_accessor :api_request_time - attr_accessor :capture_env - attr_accessor :capture_headers - attr_accessor :current_user_email_method - attr_accessor :current_user_id_method - attr_accessor :current_user_method - attr_accessor :current_user_username_method - attr_accessor :default_tags - attr_accessor :disable_send - attr_accessor :disable_start_message - attr_accessor :disabled_spies - attr_accessor :environment - attr_accessor :filter_exception_types - attr_accessor :framework_name - attr_accessor :framework_version - attr_accessor :hostname - attr_accessor :http_compression - attr_accessor :instrument - attr_accessor :instrumented_rake_tasks - attr_accessor :log_level - attr_accessor :log_path + attr_accessor :__view_paths, :__root_path attr_accessor :logger - attr_accessor :metrics_interval - attr_accessor :pool_size - attr_accessor :proxy_address - attr_accessor :proxy_headers - attr_accessor :proxy_password - attr_accessor :proxy_port - attr_accessor :proxy_username - attr_accessor :server_ca_cert - attr_accessor :service_name - attr_accessor :service_version - attr_accessor :source_lines_error_app_frames - attr_accessor :source_lines_error_library_frames - attr_accessor :source_lines_span_app_frames - attr_accessor :source_lines_span_library_frames - attr_accessor :stack_trace_limit - attr_accessor :transaction_max_spans - attr_accessor :transaction_sample_rate - attr_accessor :verify_server_cert - attr_reader :capture_body - attr_reader :custom_key_filters - attr_reader :ignore_url_patterns - attr_reader :span_frames_min_duration - attr_reader :span_frames_min_duration_us + attr_reader :options - attr_writer :alert_logger - - attr_accessor :view_paths - attr_accessor :root_path - - alias :active? :active - alias :capture_body? :capture_body - alias :capture_headers? :capture_headers - alias :capture_env? :capture_env - alias :disable_send? :disable_send - alias :disable_start_message? :disable_start_message - alias :http_compression? :http_compression - alias :instrument? :instrument - alias :verify_server_cert? :verify_server_cert - - def alert_logger - @alert_logger ||= PrefixedLogger.new($stdout, prefix: Logging::PREFIX) + def assign(update) + return unless update + update.each { |key, value| send(:"#{key}=", value) } end - def app=(app) - case app_type?(app) - when :sinatra - set_sinatra(app) - when :rails - set_rails(app) - else - self.service_name = 'ruby' - end - end - - def app_type?(app) - if defined?(Rails::Application) && app.is_a?(Rails::Application) - return :rails - end - - if app.is_a?(Class) && app.superclass.to_s == 'Sinatra::Base' - return :sinatra - end - - nil - end - - def use_ssl? - server_url.start_with?('https') - end - - def custom_key_filters=(filters) - @custom_key_filters = Array(filters).map(&Regexp.method(:new)) - end - - def ignore_url_patterns=(strings) - @ignore_url_patterns = Array(strings).map(&Regexp.method(:new)) - end - # rubocop:disable Metrics/MethodLength def available_spies %w[ delayed_job elasticsearch @@ -269,185 +132,136 @@ def enabled_spies available_spies - disabled_spies end - def span_frames_min_duration=(duration) - @span_frames_min_duration = duration - @span_frames_min_duration_us = duration * 1_000_000 + def method_missing(name, *args) + return super unless DEPRECATED_OPTIONS.include?(name) + warn "The option `#{name}' has been removed." end - def span_frames_min_duration? - span_frames_min_duration != 0 - end - - DEPRECATED_OPTIONS = %i[ - compression_level= - compression_minimum_size= - debug_http= - debug_transactions= - flush_interval= - http_open_timeout= - http_read_timeout= - enabled_environments= - disable_environment_warning= - ].freeze - - def respond_to_missing?(name) - return true if DEPRECATED_OPTIONS.include? name - return true if name.to_s.end_with?('=') - false - end - - def method_missing(name, *args) - if DEPRECATED_OPTIONS.include?(name) - alert_logger.warn "The option `#{name}' has been removed." - return + def app=(app) + case app_type?(app) + when :sinatra + set_sinatra(app) + when :rails + set_rails(app) + else + self.service_name = 'ruby' end - - if name.to_s.end_with?('=') - raise ConfigError, "No such option `#{name.to_s.delete('=')}'" - end - - super end - def collect_metrics? - metrics_interval > 0 - end - # rubocop:disable Metrics/MethodLength def capture_body=(value) if value =~ /(all|transactions|errors|off)/ - @capture_body = value + set(:capture_body, value) return end case value when true - alert_logger.warn "Boolean value for option `capture_body' has " \ + warn "Boolean value for option `capture_body' has " \ "been deprecated. Setting to 'all'" - @capture_body = 'all' + self.capture_body = 'all' when false - alert_logger.warn "Boolean value for option `capture_body' has " \ + warn "Boolean value for option `capture_body' has " \ "been deprecated. Setting to 'off'" - @capture_body = 'off' + self.capture_body = 'off' else - default = DEFAULTS[:capture_body] - alert_logger.warn "Unknown value `#{value}' for option "\ + default = options[:capture_body].default + warn "Unknown value `#{value}' for option "\ "`capture_body'. Defaulting to `#{default}'" - @capture_body = default + self.capture_body = default end end # rubocop:enable Metrics/MethodLength - private + def use_ssl? + server_url.start_with?('https') + end - def assign(options) - options.each do |key, value| - send("#{key}=", value) - end + def collect_metrics? + metrics_interval > 0 end - def set_defaults - assign(DEFAULTS) + def span_frames_min_duration? + span_frames_min_duration != 0 end - # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity - # rubocop:disable Metrics/AbcSize - def set_from_env - ENV_TO_KEY.each do |env_key, key| - next unless (value = ENV[env_key]) + def span_frames_min_duration=(value) + super + @span_frames_min_duration_us = nil + end - type, key = key if key.is_a? Array + def span_frames_min_duration_us + @span_frames_min_duration_us ||= span_frames_min_duration * 1_000_000 + end - value = - case type - when :int then value.to_i - when :float then value.to_f - when :bool then !%w[0 false].include?(value.strip.downcase) - when :list then value.split(/[ ,]/) - when :dict then Hash[value.split(/[&,]/).map { |kv| kv.split('=') }] - else value - end + def inspect + super.split.first + '>' + end - send("#{key}=", value) + private + + def load_config_file + return unless File.exist?(config_file) + + config = YAML.safe_load(ERB.new(File.read(config_file)).result) + assign(config) + end + + def load_env + @options.values.each_with_object({}) do |option, opts| + next unless (value = ENV[option.env_key]) + opts[option.key] = value end end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity - def set_from_args(options) - assign(options) - rescue ConfigError => e - alert_logger.warn format( - 'Failed to configure from arguments: %s', - e.message - ) + def build_logger + Logger.new(log_path == '-' ? STDOUT : log_path).tap do |logger| + logger.level = log_level + end end - def set_from_config_file - return unless File.exist?(config_file) - assign(YAML.safe_load(ERB.new(File.read(config_file)).result) || {}) - rescue ConfigError => e - alert_logger.warn format( - 'Failed to configure from config file: %s', - e.message - ) + def app_type?(app) + if defined?(Rails::Application) && app.is_a?(Rails::Application) + return :rails + end + + if app.is_a?(Class) && app.superclass.to_s == 'Sinatra::Base' + return :sinatra + end + + nil end def set_sinatra(app) self.service_name = format_name(service_name || app.to_s) self.framework_name = framework_name || 'Sinatra' self.framework_version = framework_version || Sinatra::VERSION - self.root_path = Dir.pwd + self.__root_path = Dir.pwd end def set_rails(app) # rubocop:disable Metrics/AbcSize self.service_name ||= format_name(service_name || rails_app_name(app)) self.framework_name ||= 'Ruby on Rails' self.framework_version ||= Rails::VERSION::STRING self.logger ||= Rails.logger - self.root_path = Rails.root.to_s - self.view_paths = app.config.paths['app/views'].existent + self.__root_path = Rails.root.to_s + self.__view_paths = app.config.paths['app/views'].existent end def rails_app_name(app) if Rails::VERSION::MAJOR >= 6 app.class.module_parent_name else app.class.parent_name end end - def build_logger - logger = Logger.new(log_path == '-' ? STDOUT : log_path) - logger.level = log_level - - self.logger = logger - end - def format_name(str) str && str.gsub('::', '_') - end - - def normalize_durations - DURATION_KEYS.each do |key| - value = send(key).to_s - default_unit = DURATION_DEFAULT_UNITS.fetch(key, 's') - duration = Duration.parse(value, default_unit: default_unit) - send("#{key}=", duration.seconds) - end - end - - def normalize_sizes - SIZE_KEYS.each do |key| - value = send(key).to_s - default_unit = SIZE_DEFAULT_UNITS.fetch(key, 'b') - size = Size.parse(value, default_unit: default_unit) - send("#{key}=", size.bytes) - end end end # rubocop:enable Metrics/ClassLength end