require 'uri'
require_relative 'settings'
require_relative 'ext'
require_relative '../transport/ext'
module Datadog
module Core
module Configuration
# This class unifies all the different ways that users can configure how we talk to the agent.
#
# It has quite a lot of complexity, but this complexity just reflects the actual complexity we have around our
# configuration today. E.g., this is just all of the complexity regarding agent settings gathered together in a
# single place. As we deprecate more and more of the different ways that these things can be configured,
# this class will reflect that simplification as well.
#
# Whenever there is a conflict (different configurations are provided in different orders), it MUST warn the users
# about it and pick a value based on the following priority: code > environment variable > defaults.
# DEV-2.0: The deprecated_for_removal_transport_configuration_proc should be removed.
class AgentSettingsResolver
AgentSettings = \
Struct.new(
:adapter,
:ssl,
:hostname,
:port,
:uds_path,
:timeout_seconds,
:deprecated_for_removal_transport_configuration_proc,
) do
def initialize(
adapter:,
ssl:,
hostname:,
port:,
uds_path:,
timeout_seconds:,
deprecated_for_removal_transport_configuration_proc:
)
super(
adapter,
ssl,
hostname,
port,
uds_path,
timeout_seconds,
deprecated_for_removal_transport_configuration_proc
)
freeze
end
end
def self.call(settings, logger: Datadog.logger)
new(settings, logger: logger).send(:call)
end
private
attr_reader \
:logger,
:settings
def initialize(settings, logger: Datadog.logger)
@settings = settings
@logger = logger
end
def call
# A transport_options proc configured for unix domain socket overrides most of the logic on this file
if transport_options.adapter == Datadog::Core::Transport::Ext::UnixSocket::ADAPTER
return AgentSettings.new(
adapter: Datadog::Core::Transport::Ext::UnixSocket::ADAPTER,
ssl: false,
hostname: nil,
port: nil,
uds_path: transport_options.uds_path,
timeout_seconds: timeout_seconds,
deprecated_for_removal_transport_configuration_proc: nil,
)
end
AgentSettings.new(
adapter: adapter,
ssl: ssl?,
hostname: hostname,
port: port,
uds_path: uds_path,
timeout_seconds: timeout_seconds,
# NOTE: When provided, the deprecated_for_removal_transport_configuration_proc can override all
# values above (ssl, hostname, port, timeout), or even make them irrelevant (by using an unix socket or
# enabling test mode instead).
# That is the main reason why it is deprecated -- it's an opaque function that may set a bunch of settings
# that we know nothing of until we actually call it.
deprecated_for_removal_transport_configuration_proc: deprecated_for_removal_transport_configuration_proc,
)
end
def adapter
if should_use_uds?
Datadog::Core::Transport::Ext::UnixSocket::ADAPTER
else
Datadog::Core::Transport::Ext::HTTP::ADAPTER
end
end
def configured_hostname
return @configured_hostname if defined?(@configured_hostname)
@configured_hostname = pick_from(
DetectedConfiguration.new(
friendly_name: "'c.tracing.transport_options'",
value: transport_options.hostname,
),
DetectedConfiguration.new(
friendly_name: "'c.agent.host'",
value: settings.agent.host
),
DetectedConfiguration.new(
friendly_name: "#{Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_URL} environment variable",
value: parsed_http_url && parsed_http_url.hostname
),
DetectedConfiguration.new(
friendly_name: "#{Datadog::Core::Configuration::Ext::Transport::ENV_DEFAULT_HOST} environment variable",
value: ENV[Datadog::Core::Configuration::Ext::Transport::ENV_DEFAULT_HOST]
)
)
end
def configured_port
return @configured_port if defined?(@configured_port)
@configured_port = pick_from(
try_parsing_as_integer(
friendly_name: "'c.tracing.transport_options'",
value: transport_options.port,
),
try_parsing_as_integer(
friendly_name: '"c.agent.port"',
value: settings.agent.port,
),
DetectedConfiguration.new(
friendly_name: "#{Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_URL} environment variable",
value: parsed_http_url && parsed_http_url.port,
),
try_parsing_as_integer(
friendly_name: "#{Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_PORT} environment variable",
value: ENV[Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_PORT],
)
)
end
def try_parsing_as_integer(value:, friendly_name:)
value =
begin
Integer(value) if value
rescue ArgumentError, TypeError
log_warning("Invalid value for #{friendly_name} (#{value.inspect}). Ignoring this configuration.")
nil
end
DetectedConfiguration.new(friendly_name: friendly_name, value: value)
end
def ssl?
transport_options.ssl ||
(!parsed_url.nil? && parsed_url.scheme == 'https')
end
def hostname
configured_hostname || (should_use_uds? ? nil : Datadog::Core::Configuration::Ext::Agent::HTTP::DEFAULT_HOST)
end
def port
configured_port || (should_use_uds? ? nil : Datadog::Core::Configuration::Ext::Agent::HTTP::DEFAULT_PORT)
end
# Unix socket path in the file system
def uds_path
if mixed_http_and_uds?
nil
elsif parsed_url && unix_scheme?(parsed_url)
path = parsed_url.to_s
# Some versions of the built-in uri gem leave the original url untouched, and others remove the //, so this
# supports both
if path.start_with?('unix://')
path.sub('unix://', '')
else
path.sub('unix:', '')
end
else
uds_fallback
end
end
# Defaults to +nil+, letting the adapter choose what default
# works best in their case.
def timeout_seconds
transport_options.timeout_seconds
end
# In transport_options, we try to invoke the transport_options proc and get its configuration. In case that
# doesn't work, we include the proc directly in the agent settings result.
def deprecated_for_removal_transport_configuration_proc
transport_options_settings if transport_options_settings.is_a?(Proc) && transport_options.adapter.nil?
end
def transport_options_settings
@transport_options_settings ||= begin
settings.tracing.transport_options if settings.respond_to?(:tracing) && settings.tracing
end
end
# We only use the default unix socket if it is already present.
# This is by design, as we still want to use the default host:port if no unix socket is present.
def uds_fallback
return @uds_fallback if defined?(@uds_fallback)
@uds_fallback =
if configured_hostname.nil? &&
configured_port.nil? &&
deprecated_for_removal_transport_configuration_proc.nil? &&
File.exist?(Datadog::Core::Configuration::Ext::Agent::UnixSocket::DEFAULT_PATH)
Datadog::Core::Configuration::Ext::Agent::UnixSocket::DEFAULT_PATH
end
end
def should_use_uds?
can_use_uds? && !mixed_http_and_uds?
end
def can_use_uds?
parsed_url && unix_scheme?(parsed_url) ||
# If no agent settings have been provided, we try to connect using a local unix socket.
# We only do so if the socket is present when `ddtrace` runs.
!uds_fallback.nil?
end
def parsed_url
return @parsed_url if defined?(@parsed_url)
unparsed_url_from_env = ENV[Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_URL]
@parsed_url =
if unparsed_url_from_env
parsed = URI.parse(unparsed_url_from_env)
if http_scheme?(parsed) || unix_scheme?(parsed)
parsed
else
# rubocop:disable Layout/LineLength
log_warning(
"Invalid URI scheme '#{parsed.scheme}' for #{Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_URL} " \
"environment variable ('#{unparsed_url_from_env}'). " \
"Ignoring the contents of #{Datadog::Core::Configuration::Ext::Agent::ENV_DEFAULT_URL}."
)
# rubocop:enable Layout/LineLength
nil
end
end
end
def pick_from(*configurations_in_priority_order)
detected_configurations_in_priority_order = configurations_in_priority_order.select(&:value?)
if detected_configurations_in_priority_order.any?
warn_if_configuration_mismatch(detected_configurations_in_priority_order)
# The configurations are listed in priority, so we only need to look at the first; if there's more than
# one, we emit a warning above
detected_configurations_in_priority_order.first.value
end
end
def warn_if_configuration_mismatch(detected_configurations_in_priority_order)
return unless detected_configurations_in_priority_order.map(&:value).uniq.size > 1
log_warning(
'Configuration mismatch: values differ between ' \
"#{detected_configurations_in_priority_order
.map { |config| "#{config.friendly_name} (#{config.value.inspect})" }.join(' and ')}" \
". Using #{detected_configurations_in_priority_order.first.value.inspect} and ignoring other configuration."
)
end
def log_warning(message)
logger.warn(message) if logger
end
def http_scheme?(uri)
['http', 'https'].include?(uri.scheme)
end
# Expected to return nil (not false!) when it's not http
def parsed_http_url
parsed_url if parsed_url && http_scheme?(parsed_url)
end
def unix_scheme?(uri)
uri.scheme == 'unix'
end
# When we have mixed settings for http/https and uds, we print a warning and ignore the uds settings
def mixed_http_and_uds?
return @mixed_http_and_uds if defined?(@mixed_http_and_uds)
@mixed_http_and_uds = (configured_hostname || configured_port) && can_use_uds?
if @mixed_http_and_uds
warn_if_configuration_mismatch(
[
DetectedConfiguration.new(
friendly_name: 'configuration of hostname/port for http/https use',
value: "hostname: '#{hostname}', port: '#{port}'",
),
DetectedConfiguration.new(
friendly_name: 'configuration for unix domain socket',
value: parsed_url.to_s,
),
]
)
end
@mixed_http_and_uds
end
# The settings.tracing.transport_options allows users to have full control over the settings used to
# communicate with the agent. In the general case, we can't extract the configuration from this proc, but
# in the specific case of the http and unix socket adapters we can, and we use this method together with the
# `TransportOptionsResolver` to call the proc and extract its information.
def transport_options
return @transport_options if defined?(@transport_options)
transport_options_proc = transport_options_settings
@transport_options = TransportOptions.new
if transport_options_proc.is_a?(Proc)
begin
transport_options_proc.call(TransportOptionsResolver.new(@transport_options))
rescue NoMethodError => e
if logger
logger.debug do
'Could not extract configuration from transport_options proc. ' \
"Cause: #{e.class.name} #{e.message} Source: #{Array(e.backtrace).first}"
end
end
# Reset the object; we shouldn't return the same one we passed into the proc as it may have
# some partial configuration and we want all-or-nothing.
@transport_options = TransportOptions.new
end
end
@transport_options.freeze
end
# Represents a given configuration value and where we got it from
class DetectedConfiguration
attr_reader :friendly_name, :value
def initialize(friendly_name:, value:)
@friendly_name = friendly_name
@value = value
freeze
end
def value?
!value.nil?
end
end
private_constant :DetectedConfiguration
# Used to contain information extracted from the transport_options proc (see #transport_options above)
TransportOptions = Struct.new(:adapter, :hostname, :port, :timeout_seconds, :ssl, :uds_path)
private_constant :TransportOptions
# Used to extract information from the transport_options proc (see #transport_options above)
class TransportOptionsResolver
def initialize(transport_options)
@transport_options = transport_options
end
def adapter(kind_or_custom_adapter, *args, **kwargs)
case kind_or_custom_adapter
when Datadog::Core::Configuration::Ext::Agent::HTTP::ADAPTER
@transport_options.adapter = Datadog::Core::Configuration::Ext::Agent::HTTP::ADAPTER
@transport_options.hostname = args[0] || kwargs[:hostname]
@transport_options.port = args[1] || kwargs[:port]
@transport_options.timeout_seconds = kwargs[:timeout]
@transport_options.ssl = kwargs[:ssl]
when Datadog::Core::Configuration::Ext::Agent::UnixSocket::ADAPTER
@transport_options.adapter = Datadog::Core::Configuration::Ext::Agent::UnixSocket::ADAPTER
@transport_options.uds_path = args[0] || kwargs[:uds_path]
end
nil
end
end
end
end
end
end