# frozen_string_literal: true 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/bytes' require 'elastic_apm/config/regexp_list' module ElasticAPM # rubocop:disable Metrics/ClassLength # @api private class Config extend Options 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 # rubocop:disable Metrics/LineLength, Layout/ExtraSpacing option :config_file, type: :string, default: 'config/elastic_apm.yml' option :server_url, type: :string, default: 'http://localhost:8200' option :secret_token, type: :string 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 # rubocop:disable Metrics/MethodLength def initialize(options = {}) @options = load_schema custom_logger = options.delete(:logger) assign(options) # Pick out config_file specifically as we need it now to load it, # but still need the other env vars to have precedence env = load_env if (env_config_file = env.delete(:config_file)) self.config_file = env_config_file end assign(load_config_file) assign(env) yield self if block_given? @logger = custom_logger || build_logger @__view_paths = [] @__root_path = Dir.pwd end # rubocop:enable Metrics/MethodLength attr_accessor :__view_paths, :__root_path attr_accessor :logger attr_reader :options def assign(update) return unless update update.each { |key, value| send(:"#{key}=", value) } end # rubocop:disable Metrics/MethodLength def available_spies %w[ delayed_job elasticsearch faraday http json mongo net_http redis sequel sidekiq sinatra tilt rake ] end # rubocop:enable Metrics/MethodLength def enabled_spies available_spies - disabled_spies end def method_missing(name, *args) return super unless DEPRECATED_OPTIONS.include?(name) warn "The option `#{name}' has been removed." 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 # rubocop:disable Metrics/MethodLength def capture_body=(value) if value =~ /(all|transactions|errors|off)/ set(:capture_body, value) return end case value when true warn "Boolean value for option `capture_body' has " \ "been deprecated. Setting to 'all'" self.capture_body = 'all' when false warn "Boolean value for option `capture_body' has " \ "been deprecated. Setting to 'off'" self.capture_body = 'off' else default = options[:capture_body].default warn "Unknown value `#{value}' for option "\ "`capture_body'. Defaulting to `#{default}'" self.capture_body = default end end # rubocop:enable Metrics/MethodLength def use_ssl? server_url.start_with?('https') end def collect_metrics? metrics_interval > 0 end def span_frames_min_duration? span_frames_min_duration != 0 end def span_frames_min_duration=(value) super @span_frames_min_duration_us = nil end def span_frames_min_duration_us @span_frames_min_duration_us ||= span_frames_min_duration * 1_000_000 end def inspect super.split.first + '>' end private def load_config_file return unless File.exist?(config_file) read = File.read(config_file) evaled = ERB.new(read).result YAML.safe_load(evaled) 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 def build_logger Logger.new(log_path == '-' ? STDOUT : log_path).tap do |logger| logger.level = log_level 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 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 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 end def rails_app_name(app) if Rails::VERSION::MAJOR >= 6 app.class.module_parent_name else app.class.parent_name end end def format_name(str) str && str.gsub('::', '_') end end # rubocop:enable Metrics/ClassLength end