lib/elastic_apm/config.rb



# frozen_string_literal: true

require 'logger'
require 'yaml'

module ElasticAPM
  # rubocop:disable Metrics/ClassLength
  # @api private
  class Config
    DEFAULTS = {
      config_file: 'config/elastic_apm.yml',
      server_url: 'http://localhost:8200',

      environment: ENV['RAILS_ENV'] || ENV['RACK_ENV'],
      enabled_environments: %w[production],

      log_path: '-',
      log_level: Logger::INFO,

      max_queue_size: 500,
      flush_interval: 10,
      transaction_sample_rate: 1.0,
      transaction_max_spans: 500,
      filter_exception_types: [],

      http_read_timeout: 120,
      http_open_timeout: 60,
      debug_transactions: false,
      debug_http: false,
      verify_server_cert: true,
      http_compression: true,
      compression_minimum_size: 1024 * 5,
      compression_level: 6,

      source_lines_error_app_frames: 5,
      source_lines_span_app_frames: 5,
      source_lines_error_library_frames: 0,
      source_lines_span_library_frames: 0,

      disabled_spies: %w[json],

      current_user_id_method: :id,
      current_user_email_method: :email,
      current_user_username_method: :username,

      view_paths: [],
      root_path: Dir.pwd
    }.freeze

    ENV_TO_KEY = {
      'ELASTIC_APM_SERVER_URL' => 'server_url',
      'ELASTIC_APM_SECRET_TOKEN' => 'secret_token',

      'ELASTIC_APM_SERVICE_NAME' => 'service_name',
      'ELASTIC_APM_SERVICE_VERSION' => 'service_version',
      'ELASTIC_APM_ENVIRONMENT' => 'environment',
      'ELASTIC_APM_ENABLED_ENVIRONMENTS' => [:list, 'enabled_environments'],
      'ELASTIC_APM_FRAMEWORK_NAME' => 'framework_name',
      'ELASTIC_APM_FRAMEWORK_VERSION' => 'framework_version',
      'ELASTIC_APM_HOSTNAME' => 'hostname',

      'ELASTIC_APM_SOURCE_LINES_ERROR_APP_FRAMES' =>
        [:int, 'source_lines_error_app_frames'],
      'ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES' =>
        [:int, 'source_lines_span_app_frames'],
      'ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES' =>
        [:int, 'source_lines_error_library_frames'],
      'ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES' =>
        [:int, 'source_lines_span_library_frames'],

      'ELASTIC_APM_MAX_QUEUE_SIZE' => [:int, 'max_queue_size'],
      'ELASTIC_APM_FLUSH_INTERVAL' => 'flush_interval',
      'ELASTIC_APM_TRANSACTION_SAMPLE_RATE' =>
        [:float, 'transaction_sample_rate'],
      'ELASTIC_APM_VERIFY_SERVER_CERT' => [:bool, 'verify_server_cert'],
      'ELASTIC_APM_TRANSACTION_MAX_SPANS' => [:int, 'transaction_max_spans'],

      'ELASTIC_APM_DISABLED_SPIES' => [:list, 'disabled_spies']
    }.freeze

    def initialize(options = {})
      set_defaults

      set_from_args(options)
      set_from_config_file
      set_from_env

      yield self if block_given?
    end

    attr_accessor :config_file

    attr_accessor :server_url
    attr_accessor :secret_token

    attr_accessor :service_name
    attr_accessor :service_version
    attr_accessor :environment
    attr_accessor :framework_name
    attr_accessor :framework_version
    attr_accessor :hostname
    attr_accessor :enabled_environments

    attr_accessor :log_path
    attr_accessor :log_level

    attr_accessor :max_queue_size
    attr_accessor :flush_interval
    attr_accessor :transaction_sample_rate
    attr_accessor :transaction_max_spans
    attr_accessor :verify_server_cert
    attr_accessor :filter_exception_types

    attr_accessor :http_read_timeout
    attr_accessor :http_open_timeout
    attr_accessor :debug_transactions
    attr_accessor :debug_http
    attr_accessor :http_compression
    attr_accessor :compression_minimum_size
    attr_accessor :compression_level

    attr_accessor :source_lines_error_app_frames
    attr_accessor :source_lines_span_app_frames
    attr_accessor :source_lines_error_library_frames
    attr_accessor :source_lines_span_library_frames

    attr_accessor :disabled_spies

    attr_accessor :view_paths
    attr_accessor :root_path

    attr_accessor :current_user_method
    attr_accessor :current_user_id_method
    attr_accessor :current_user_email_method
    attr_accessor :current_user_username_method

    attr_reader   :logger

    alias :verify_server_cert? :verify_server_cert

    def app=(app)
      case app_type?(app)
      when :sinatra
        set_sinatra(app)
      when :rails
        set_rails(app)
      else
        # TODO: define custom?
        self.service_name = 'ruby'
      end
    end

    def app_type?(app)
      if defined?(::Rails) && 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 logger=(logger)
      @logger = logger || build_logger(log_path, log_level)
    end

    # rubocop:disable Metrics/MethodLength
    def available_spies
      %w[
        action_dispatch
        delayed_job
        elasticsearch
        json
        mongo
        net_http
        redis
        sequel
        sidekiq
        sinatra
        tilt
      ]
    end
    # rubocop:enable Metrics/MethodLength

    def enabled_spies
      available_spies - disabled_spies
    end

    private

    def assign(options)
      options.each do |key, value|
        send("#{key}=", value)
      end
    end

    def set_defaults
      assign(DEFAULTS)
    end

    # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
    def set_from_env
      ENV_TO_KEY.each do |env_key, key|
        next unless (value = ENV[env_key])

        type, key = key if key.is_a? Array

        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(/[ ,]/)
          else value
          end

        send("#{key}=", value)
      end
    end
    # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity

    def set_from_args(options)
      assign(options)
    end

    def set_from_config_file
      return unless File.exist?(config_file)
      assign(YAML.load_file(config_file) || {})
    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 || app.class.parent_name)
      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 build_logger(path, level)
      logger = Logger.new(path == '-' ? STDOUT : path)
      logger.level = level
      logger
    end

    def format_name(str)
      str.gsub('::', '_')
    end
  end
  # rubocop:enable Metrics/ClassLength
end