# frozen_string_literal: true
require 'logger'
require 'yaml'
require 'erb'
require 'elastic_apm/util/prefixed_logger'
require 'elastic_apm/config/duration'
require 'elastic_apm/config/size'
module ElasticAPM
class ConfigError < StandardError; end
# rubocop:disable Metrics/ClassLength
# @api private
class Config
DEFAULTS = {
config_file: 'config/elastic_apm.yml',
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,
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',
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_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_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
].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
def initialize(options = {})
set_defaults
set_from_args(options)
set_from_config_file
set_from_env
normalize_durations
normalize_sizes
yield self if block_given?
build_logger if logger.nil?
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 :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 :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 :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_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 :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)
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[
action_dispatch
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 span_frames_min_duration=(duration)
@span_frames_min_duration = duration
@span_frames_min_duration_us = duration * 1_000_000
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
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
return
end
case value
when true
alert_logger.warn "Boolean value for option `capture_body' has " \
"been deprecated. Setting to 'all'"
@capture_body = 'all'
when false
alert_logger.warn "Boolean value for option `capture_body' has " \
"been deprecated. Setting to 'off'"
@capture_body = 'off'
else
default = DEFAULTS[:capture_body]
alert_logger.warn "Unknown value `#{value}' for option "\
"`capture_body'. Defaulting to `#{default}'"
@capture_body = default
end
end
# rubocop:enable Metrics/MethodLength
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
# rubocop:disable Metrics/AbcSize
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(/[ ,]/)
when :dict then Hash[value.split(/[&,]/).map { |kv| kv.split('=') }]
else value
end
send("#{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
)
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
)
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
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