From 2a5689ed74a95d5d7ff51414c439964b606b6f53 Mon Sep 17 00:00:00 2001 From: David Elner Date: Sun, 23 Jan 2022 18:57:37 -0500 Subject: [PATCH] Refactored: Datadog::Configuration --> Datadog::Core::Configuration --- .rubocop.yml | 4 + docs/GettingStarted.md | 14 +- lib/datadog/ci.rb | 12 +- lib/datadog/ci/extensions.rb | 8 +- lib/datadog/core.rb | 73 +++ lib/datadog/core/configuration.rb | 302 +++++++++ .../configuration/agent_settings_resolver.rb | 311 +++++++++ lib/datadog/core/configuration/base.rb | 88 +++ lib/datadog/core/configuration/components.rb | 381 +++++++++++ .../core/configuration/dependency_resolver.rb | 27 + lib/datadog/core/configuration/option.rb | 68 ++ .../core/configuration/option_definition.rb | 125 ++++ .../configuration/option_definition_set.rb | 21 + lib/datadog/core/configuration/option_set.rb | 9 + lib/datadog/core/configuration/options.rb | 117 ++++ lib/datadog/core/configuration/settings.rb | 619 ++++++++++++++++++ .../core/configuration/validation_proxy.rb | 101 +++ lib/datadog/core/extensions.rb | 15 + lib/datadog/core/pin.rb | 75 +++ lib/datadog/tracing.rb | 77 +-- lib/ddtrace.rb | 45 +- lib/ddtrace/configuration.rb | 242 ------- .../configuration/agent_settings_resolver.rb | 309 --------- lib/ddtrace/configuration/base.rb | 86 --- lib/ddtrace/configuration/components.rb | 375 ----------- .../configuration/dependency_resolver.rb | 25 - lib/ddtrace/configuration/option.rb | 66 -- .../configuration/option_definition.rb | 123 ---- .../configuration/option_definition_set.rb | 19 - lib/ddtrace/configuration/option_set.rb | 7 - lib/ddtrace/configuration/options.rb | 115 ---- lib/ddtrace/configuration/settings.rb | 610 ----------------- lib/ddtrace/configuration/validation_proxy.rb | 99 --- lib/ddtrace/contrib/configuration/settings.rb | 4 +- lib/ddtrace/contrib/elasticsearch/patcher.rb | 3 +- lib/ddtrace/contrib/extensions.rb | 9 +- .../contrib/grpc/datadog_interceptor.rb | 4 +- lib/ddtrace/contrib/http/instrumentation.rb | 2 +- .../contrib/httpclient/instrumentation.rb | 2 +- lib/ddtrace/contrib/httprb/instrumentation.rb | 2 +- .../contrib/mongodb/instrumentation.rb | 3 +- lib/ddtrace/contrib/mongodb/subscribers.rb | 2 +- lib/ddtrace/contrib/mysql2/instrumentation.rb | 2 +- lib/ddtrace/contrib/qless/qless_job.rb | 2 +- lib/ddtrace/contrib/qless/tracer_cleaner.rb | 2 +- lib/ddtrace/contrib/rails/framework.rb | 1 - lib/ddtrace/contrib/redis/instrumentation.rb | 4 +- lib/ddtrace/contrib/resque/resque_job.rb | 4 +- lib/ddtrace/contrib/sequel/database.rb | 2 +- lib/ddtrace/contrib/sequel/dataset.rb | 2 +- .../contrib/sucker_punch/instrumentation.rb | 2 - .../distributed_tracing/headers/headers.rb | 2 +- .../distributed_tracing/headers/helpers.rb | 2 +- lib/ddtrace/pin.rb | 72 -- lib/ddtrace/profiling.rb | 12 +- lib/ddtrace/propagation/http_propagator.rb | 2 +- lib/ddtrace/sync_writer.rb | 2 +- lib/ddtrace/transport/http.rb | 9 +- lib/ddtrace/transport/http/builder.rb | 4 +- lib/ddtrace/writer.rb | 2 +- .../ci/configuration/components_spec.rb | 14 +- .../datadog/ci/configuration/settings_spec.rb | 8 +- .../ci/contrib/support/mode_helpers.rb | 8 +- spec/datadog/ci/test_spec.rb | 2 - .../agent_settings_resolver_spec.rb | 8 +- .../core}/configuration/base_spec.rb | 6 +- .../core}/configuration/components_spec.rb | 23 +- .../configuration/dependency_resolver_spec.rb | 2 +- .../option_definition_set_spec.rb | 12 +- .../configuration/option_definition_spec.rb | 16 +- .../core}/configuration/option_set_spec.rb | 2 +- .../core}/configuration/option_spec.rb | 4 +- .../core}/configuration/options_spec.rb | 16 +- .../core}/configuration/settings_spec.rb | 11 +- .../core}/configuration_spec.rb | 65 +- spec/{ddtrace => datadog/core}/pin_spec.rb | 4 +- .../core/workers/interval_loop_spec.rb | 2 +- spec/datadog/core_spec.rb | 80 +++ .../integration_spec.rb} | 3 +- spec/datadog/tracing_spec.rb | 59 +- .../contrib/configuration/settings_spec.rb | 2 +- .../contrib/elasticsearch/transport_spec.rb | 2 +- spec/ddtrace/contrib/extensions_spec.rb | 2 +- spec/ddtrace/contrib/http/request_spec.rb | 2 +- spec/ddtrace/contrib/mongodb/client_spec.rb | 2 +- spec/ddtrace/contrib/mysql2/patcher_spec.rb | 2 +- .../ddtrace/contrib/rails/redis_cache_spec.rb | 2 +- spec/ddtrace/contrib/redis/miniapp_spec.rb | 2 +- .../contrib/sequel/configuration_spec.rb | 2 +- .../contrib/sequel/instrumentation_spec.rb | 2 +- spec/ddtrace/event_spec.rb | 2 - spec/ddtrace/profiling/recorder_spec.rb | 2 +- .../trace_identifiers/ddtrace_spec.rb | 2 +- .../http/adapters/net_integration_spec.rb | 6 +- .../transport/http/integration_spec.rb | 10 +- spec/ddtrace/profiling/transport/http_spec.rb | 2 +- spec/ddtrace/span_operation_spec.rb | 4 - spec/ddtrace/sync_writer_spec.rb | 2 +- spec/ddtrace/trace_operation_spec.rb | 8 - spec/ddtrace/transport/http/builder_spec.rb | 2 +- spec/ddtrace/transport/http_spec.rb | 14 +- spec/ddtrace_spec.rb | 28 - yard/extensions.rb | 4 +- 103 files changed, 2669 insertions(+), 2509 deletions(-) create mode 100644 lib/datadog/core.rb create mode 100644 lib/datadog/core/configuration.rb create mode 100644 lib/datadog/core/configuration/agent_settings_resolver.rb create mode 100644 lib/datadog/core/configuration/base.rb create mode 100644 lib/datadog/core/configuration/components.rb create mode 100644 lib/datadog/core/configuration/dependency_resolver.rb create mode 100644 lib/datadog/core/configuration/option.rb create mode 100644 lib/datadog/core/configuration/option_definition.rb create mode 100644 lib/datadog/core/configuration/option_definition_set.rb create mode 100644 lib/datadog/core/configuration/option_set.rb create mode 100644 lib/datadog/core/configuration/options.rb create mode 100644 lib/datadog/core/configuration/settings.rb create mode 100644 lib/datadog/core/configuration/validation_proxy.rb create mode 100644 lib/datadog/core/extensions.rb create mode 100644 lib/datadog/core/pin.rb delete mode 100644 lib/ddtrace/configuration.rb delete mode 100644 lib/ddtrace/configuration/agent_settings_resolver.rb delete mode 100644 lib/ddtrace/configuration/base.rb delete mode 100644 lib/ddtrace/configuration/components.rb delete mode 100644 lib/ddtrace/configuration/dependency_resolver.rb delete mode 100644 lib/ddtrace/configuration/option.rb delete mode 100644 lib/ddtrace/configuration/option_definition.rb delete mode 100644 lib/ddtrace/configuration/option_definition_set.rb delete mode 100644 lib/ddtrace/configuration/option_set.rb delete mode 100644 lib/ddtrace/configuration/options.rb delete mode 100644 lib/ddtrace/configuration/settings.rb delete mode 100644 lib/ddtrace/configuration/validation_proxy.rb delete mode 100644 lib/ddtrace/pin.rb rename spec/{ddtrace => datadog/core}/configuration/agent_settings_resolver_spec.rb (97%) rename spec/{ddtrace => datadog/core}/configuration/base_spec.rb (92%) rename spec/{ddtrace => datadog/core}/configuration/components_spec.rb (97%) rename spec/{ddtrace => datadog/core}/configuration/dependency_resolver_spec.rb (88%) rename spec/{ddtrace => datadog/core}/configuration/option_definition_set_spec.rb (73%) rename spec/{ddtrace => datadog/core}/configuration/option_definition_spec.rb (93%) rename spec/{ddtrace => datadog/core}/configuration/option_set_spec.rb (71%) rename spec/{ddtrace => datadog/core}/configuration/option_spec.rb (98%) rename spec/{ddtrace => datadog/core}/configuration/options_spec.rb (90%) rename spec/{ddtrace => datadog/core}/configuration/settings_spec.rb (99%) rename spec/{ddtrace => datadog/core}/configuration_spec.rb (90%) rename spec/{ddtrace => datadog/core}/pin_spec.rb (98%) create mode 100644 spec/datadog/core_spec.rb rename spec/{ddtrace_integration_spec.rb => datadog/integration_spec.rb} (98%) delete mode 100644 spec/ddtrace_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index e3781d97c43..c4bed2ea3aa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -299,6 +299,10 @@ RSpec/NestedGroups: RSpec/MessageSpies: Enabled: false +# Enforces use of `instance_double` over `double` +RSpec/VerifiedDoubles: + Enabled: false + # Enforces example line count limit. RSpec/ExampleLength: Enabled: false diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index ba81ba9aace..4aa8441299f 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -1173,7 +1173,7 @@ collection = client[:people] collection.insert_one({ name: 'Steve' }) # In case you want to override the global configuration for a certain client instance -Datadog::Tracing.configure_onto(client, **options) +Datadog.configure_onto(client, **options) ``` Where `options` is an optional `Hash` that accepts the following parameters: @@ -1270,11 +1270,11 @@ Where `options` is an optional `Hash` that accepts the following parameters: | `service_name` | Service name used for `http` instrumentation | `'net/http'` | | `split_by_domain` | Uses the request domain as the service name when set to `true`. | `false` | -If you wish to configure each connection object individually, you may use the `Datadog::Tracing.configure_onto` as it follows: +If you wish to configure each connection object individually, you may use the `Datadog.configure_onto` as it follows: ```ruby client = Net::HTTP.new(host, port) -Datadog::Tracing.configure_onto(client, **options) +Datadog.configure_onto(client, **options) ``` ### Presto @@ -1580,8 +1580,8 @@ end customer_cache = Redis.new invoice_cache = Redis.new -Datadog::Tracing.configure_onto(customer_cache, service_name: 'customer-cache') -Datadog::Tracing.configure_onto(invoice_cache, service_name: 'invoice-cache') +Datadog.configure_onto(customer_cache, service_name: 'customer-cache') +Datadog.configure_onto(invoice_cache, service_name: 'invoice-cache') # Traced call will belong to `customer-cache` service customer_cache.get(...) @@ -1729,8 +1729,8 @@ sqlite_database = Sequel.sqlite postgres_database = Sequel.connect('postgres://user:password@host:port/database_name') # Configure each database with different service names -Datadog::Tracing.configure_onto(sqlite_database, service_name: 'my-sqlite-db') -Datadog::Tracing.configure_onto(postgres_database, service_name: 'my-postgres-db') +Datadog.configure_onto(sqlite_database, service_name: 'my-sqlite-db') +Datadog.configure_onto(postgres_database, service_name: 'my-postgres-db') ``` ### Shoryuken diff --git a/lib/datadog/ci.rb b/lib/datadog/ci.rb index 0276681352f..3b67fcf7118 100644 --- a/lib/datadog/ci.rb +++ b/lib/datadog/ci.rb @@ -11,11 +11,11 @@ module CI # # To modify the configuration, use {.configure}. # - # @return [Datadog::Configuration::Settings] + # @return [Datadog::Core::Configuration::Settings] # @!attribute [r] configuration # @public_api def configuration - Datadog::Configuration::ValidationProxy::CI.new( + Datadog::Core::Configuration::ValidationProxy::CI.new( Datadog.send(:internal_configuration) ) end @@ -26,7 +26,7 @@ def configuration # c.ci_mode.enabled = true # end # ``` - # See {Datadog::Configuration::Settings} for all available options, defaults, and + # See {Datadog::Core::Configuration::Settings} for all available options, defaults, and # available environment variables for configuration. # # Only permits access to CI configuration settings; others will raise an error. @@ -43,18 +43,18 @@ def configuration # The yielded configuration `c` comes pre-populated from environment variables, if # any are applicable. # - # See {Datadog::Configuration::Settings} for all available options, defaults, and + # See {Datadog::Core::Configuration::Settings} for all available options, defaults, and # available environment variables for configuration. # # Will raise errors if invalid setting is accessed. # - # @yieldparam [Datadog::Configuration::Settings] c the mutable configuration object + # @yieldparam [Datadog::Core::Configuration::Settings] c the mutable configuration object # @return [void] # @public_api def configure # Wrap block with trace option validation wrapped_block = proc do |c| - yield(Datadog::Configuration::ValidationProxy::CI.new(c)) + yield(Datadog::Core::Configuration::ValidationProxy::CI.new(c)) end # Configure application normally diff --git a/lib/datadog/ci/extensions.rb b/lib/datadog/ci/extensions.rb index 47182f7526a..eb94f3dc39b 100644 --- a/lib/datadog/ci/extensions.rb +++ b/lib/datadog/ci/extensions.rb @@ -1,6 +1,6 @@ # typed: true -require 'ddtrace/configuration/settings' -require 'ddtrace/configuration/components' +require 'datadog/core/configuration/settings' +require 'datadog/core/configuration/components' require 'datadog/ci/configuration/settings' require 'datadog/ci/configuration/components' @@ -10,8 +10,8 @@ module CI # Extends Datadog tracing with CI features module Extensions def self.activate! - Datadog::Configuration::Settings.extend(CI::Configuration::Settings) - Datadog::Configuration::Components.prepend(CI::Configuration::Components) + Datadog::Core::Configuration::Settings.extend(CI::Configuration::Settings) + Datadog::Core::Configuration::Components.prepend(CI::Configuration::Components) end end end diff --git a/lib/datadog/core.rb b/lib/datadog/core.rb new file mode 100644 index 00000000000..a3f8767d19e --- /dev/null +++ b/lib/datadog/core.rb @@ -0,0 +1,73 @@ +# typed: strict +# TODO: Move these requires to smaller modules. +# Would be better to lazy load these; not +# all of these components will be used in +# every application. +# require 'datadog/core/buffer/cruby' +# require 'datadog/core/buffer/random' +# require 'datadog/core/buffer/thread_safe' +# require 'datadog/core/chunker' +# require 'datadog/core/configuration' +# require 'datadog/core/diagnostics/environment_logger' +# require 'datadog/core/diagnostics/ext' +# require 'datadog/core/diagnostics/health' +# require 'datadog/core/encoding' +# require 'datadog/core/environment/cgroup' +# require 'datadog/core/environment/class_count' +# require 'datadog/core/environment/container' +# require 'datadog/core/environment/ext' +# require 'datadog/core/environment/gc' +# require 'datadog/core/environment/identity' +# require 'datadog/core/environment/socket' +# require 'datadog/core/environment/thread_count' +# require 'datadog/core/environment/variable_helpers' +# require 'datadog/core/environment/vm_cache' +# require 'datadog/core/error' +# require 'datadog/core/event' +# require 'datadog/core/git/ext' +# require 'datadog/core/logger' +# require 'datadog/core/metrics/client' +# require 'datadog/core/metrics/ext' +# require 'datadog/core/metrics/helpers' +# require 'datadog/core/metrics/logging' +# require 'datadog/core/metrics/metric' +# require 'datadog/core/metrics/options' +# require 'datadog/core/pin' +# require 'datadog/core/quantization/hash' +# require 'datadog/core/quantization/http' +# require 'datadog/core/runtime/ext' +# require 'datadog/core/runtime/metrics' +# require 'datadog/core/utils' +# require 'datadog/core/utils/compression' +# require 'datadog/core/utils/database' +# require 'datadog/core/utils/forking' +# require 'datadog/core/utils/object_set' +# require 'datadog/core/utils/only_once' +# require 'datadog/core/utils/sequence' +# require 'datadog/core/utils/string_table' +# require 'datadog/core/utils/time' +# require 'datadog/core/worker' +# require 'datadog/core/workers/async' +# require 'datadog/core/workers/interval_loop' +# require 'datadog/core/workers/polling' +# require 'datadog/core/workers/queue' +# require 'datadog/core/workers/runtime_metrics' + +require 'datadog/core/extensions' + +# We must load core extensions to make certain global APIs +# accessible: both for Datadog features and the core itself. +module Datadog + extend Core::Extensions + + # Add shutdown hook: + # Ensures the Datadog components have a chance to gracefully + # shut down and cleanup before terminating the process. + at_exit do + if Interrupt === $! # rubocop:disable Style/SpecialGlobalVars is process terminating due to a ctrl+c or similar? + Datadog.send(:handle_interrupt_shutdown!) + else + Datadog.shutdown! + end + end +end diff --git a/lib/datadog/core/configuration.rb b/lib/datadog/core/configuration.rb new file mode 100644 index 00000000000..a5a676c9798 --- /dev/null +++ b/lib/datadog/core/configuration.rb @@ -0,0 +1,302 @@ +# typed: true +require 'forwardable' +require 'datadog/core/configuration/components' +require 'datadog/core/configuration/settings' +require 'datadog/core/configuration/validation_proxy' +require 'datadog/core/logger' +require 'datadog/core/pin' + +module Datadog + module Core + # Configuration provides a unique access point for configurations + module Configuration # rubocop:disable Metrics/ModuleLength + include Kernel # Ensure that kernel methods are always available (https://sorbet.org/docs/error-reference#7003) + extend Forwardable + + # Used to ensure that @components initialization/reconfiguration is performed one-at-a-time, by a single thread. + # + # This is important because components can end up being accessed from multiple application threads (for instance on + # a threaded webserver), and we don't want their initialization to clash (for instance, starting two profilers...). + # + # Note that a Mutex **IS NOT** reentrant: the same thread cannot grab the same Mutex more than once. + # This means below we are careful not to nest calls to methods that would trigger initialization and grab the lock. + # + # Every method that directly or indirectly mutates @components should be holding the lock (through + # #safely_synchronize) while doing so. + COMPONENTS_WRITE_LOCK = Mutex.new + private_constant :COMPONENTS_WRITE_LOCK + + # We use a separate lock when reading the @components, so that they continue to be accessible during reconfiguration. + # This was needed because we ran into several issues where we still needed to read the old + # components while the COMPONENTS_WRITE_LOCK was being held (see https://github.com/DataDog/dd-trace-rb/pull/1387 + # and https://github.com/DataDog/dd-trace-rb/pull/1373#issuecomment-799593022 ). + # + # Technically on MRI we could get away without this lock, but on non-MRI Rubies, we may run into issues because + # we fall into the "UnsafeDCLFactory" case of https://shipilev.net/blog/2014/safe-public-construction/ . + # Specifically, on JRuby reads from the @components do NOT have volatile semantics, and on TruffleRuby they do + # BUT just as an implementation detail, see https://github.com/jruby/jruby/wiki/Concurrency-in-jruby#volatility and + # https://github.com/DataDog/dd-trace-rb/pull/1329#issuecomment-776750377 . + # Concurrency is hard. + COMPONENTS_READ_LOCK = Mutex.new + private_constant :COMPONENTS_READ_LOCK + + attr_writer :configuration + + # Current Datadog configuration. + # + # Access to non-global configuration will raise an error. + # + # To modify the configuration, use {.configure}. + # + # @return [Datadog::Core::Configuration::Settings] + # @!attribute [r] configuration + # @public_api + def configuration + ValidationProxy::Global.new( + internal_configuration + ) + end + + # Apply global configuration changes to `Datadog`. An example of a {.configure} call: + # + # ``` + # Datadog.configure do |c| + # c.service = 'my-service' + # c.env = 'staging' + # # c.diagnostics.debug = true # Enables debug output + # end + # ``` + # + # See {Datadog::Core::Configuration::Settings} for all available options, defaults, and + # available environment variables for configuration. + # + # Only permits access to global configuration settings; others will raise an error. + # If you wish to configure a setting for a specific Datadog component (e.g. Tracing), + # use the corresponding `Datadog::COMPONENT.configure` method instead. + # + # Because many configuration changes require restarting internal components, + # invoking {.configure} is the only safe way to change `Datadog` configuration. + # + # Successive calls to {.configure} maintain the previous configuration values: + # configuration is additive between {.configure} calls. + # + # The yielded configuration `c` comes pre-populated from environment variables, if + # any are applicable. + # + # @param [Datadog::Core::Configuration::Settings] configuration the base configuration object. Provide a custom + # instance if you are managing the configuration yourself. By default, the global configuration object is used. + # @yieldparam [Datadog::Core::Configuration::Settings] c the mutable configuration object + def configure(configuration = self.configuration) + # Wrap block with global option validation + wrapped_block = proc do |c| + yield(ValidationProxy::Global.new(c)) + end + + # Configure application normally + internal_configure(&wrapped_block) + end + + # Apply configuration changes only to a specific Ruby object. + # + # Certain integrations or Datadog features may use these + # settings to customize behavior for this object. + # + # An example of a {.configure_onto} call: + # + # ``` + # client = Net::HTTP.new(host, port) + # Datadog.configure_onto(client, service_name: 'api-requests', split_by_domain: true) + # ``` + # + # In this example, it will configure the `client` object with custom options + # `service_name: 'api-requests', split_by_domain: true`. The `Net::HTTP` integration + # will then use these customized options when the `client` is used, whereas other + # clients will use the `service_name: 'http-requests'` configuration provided to the + # `Datadog.configure` call block. + # + # {.configure_onto} is used to separate cases where spans generated by certain objects + # require exceptional options. + # + # The configuration keyword arguments provided should match well known options defined + # in the integration or feature that would use them. + # + # For example, for `Datadog.configure_onto(redis_client, **opts)`, `opts` can be + # any of the options in the Redis {Datadog::Contrib::Redis::Configuration::Settings} class. + # + # @param [Object] target the object to receive configuration options + # @param [Hash] opts keyword arguments respective to the integration this object belongs to + # @public_api + def configure_onto(target, **opts) + Pin.set_on(target, **opts) + end + + # Get configuration changes applied only to a specific Ruby object, via {.configure_onto}. + # An example of an object with specific configuration: + # + # ``` + # client = Net::HTTP.new(host, port) + # Datadog.configure_onto(client, service_name: 'api-requests', split_by_domain: true) + # config = Datadog.configuration_for(client) + # config[:service_name] # => 'api-requests' + # config[:split_by_domain] # => true + # ``` + # + # @param [Object] target the object to receive configuration options + # @param [Object] option an option to retrieve from the object configuration + # @public_api + def configuration_for(target, option = nil) + pin = Pin.get_from(target) + return pin unless option + + pin[option] if pin + end + + def_delegators \ + :components, + :health_metrics, + :profiler + + def logger + # avoid initializing components if they didn't already exist + current_components = components(allow_initialization: false) + + if current_components + @temp_logger = nil + current_components.logger + else + logger_without_components + end + end + + # Gracefully shuts down all components. + # + # Components will still respond to method calls as usual, + # but might not internally perform their work after shutdown. + # + # This avoids errors being raised across the host application + # during shutdown, while allowing for graceful decommission of resources. + # + # Components won't be automatically reinitialized after a shutdown. + def shutdown! + safely_synchronize do + @components.shutdown! if components? + end + end + + protected + + def components(allow_initialization: true) + current_components = COMPONENTS_READ_LOCK.synchronize { defined?(@components) && @components } + return current_components if current_components || !allow_initialization + + safely_synchronize do |write_components| + (defined?(@components) && @components) || write_components.call(build_components(internal_configuration)) + end + end + + private + + def internal_configure(configuration = internal_configuration) + yield(configuration) + + safely_synchronize do |write_components| + write_components.call( + if components? + replace_components!(configuration, @components) + else + build_components(configuration) + end + ) + end + + configuration + end + + def internal_configuration + @configuration ||= Settings.new + end + + # Gracefully shuts down Datadog components and disposes of component references, + # allowing execution to start anew. + # + # In contrast with +#shutdown!+, components will be automatically + # reinitialized after a reset. + # + # Used internally to ensure a clean environment between test runs. + def reset! + safely_synchronize do |write_components| + @components.shutdown! if components? + write_components.call(nil) + configuration.reset! + end + end + + def safely_synchronize + # Writes to @components should only happen through this proc. Because this proc is only accessible to callers of + # safely_synchronize, this forces all writers to go through this method. + write_components = proc do |new_value| + COMPONENTS_READ_LOCK.synchronize { @components = new_value } + end + + COMPONENTS_WRITE_LOCK.synchronize do + begin + yield write_components + rescue ThreadError => e + logger_without_components.error( + 'Detected deadlock during ddtrace initialization. ' \ + 'Please report this at https://github.com/DataDog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug' \ + "\n\tSource:\n\t#{Array(e.backtrace).join("\n\t")}" + ) + nil + end + end + end + + def components? + # This does not need to grab the COMPONENTS_READ_LOCK because it's not returning the components + (defined?(@components) && @components) != nil + end + + def build_components(settings) + components = Components.new(settings) + components.startup!(settings) + components + end + + def replace_components!(settings, old) + components = Components.new(settings) + + old.shutdown!(components) + components.startup!(settings) + components + end + + def logger_without_components + # Use default logger without initializing components. + # This enables logging during initialization, otherwise we'd run into deadlocks. + @temp_logger ||= begin + logger = configuration.logger.instance || Core::Logger.new($stdout) + logger.level = configuration.diagnostics.debug ? ::Logger::DEBUG : configuration.logger.level + logger + end + end + + # Called from our at_exit hook whenever there was a pending Interrupt exception (e.g. typically due to ctrl+c) + # to print a nice message whenever we're taking a bit longer than usual to finish the process. + def handle_interrupt_shutdown! + logger = Datadog.logger + shutdown_thread = Thread.new { shutdown! } + print_message_treshold_seconds = 0.2 + + slow_shutdown = shutdown_thread.join(print_message_treshold_seconds).nil? + + if slow_shutdown + logger.info 'Reporting remaining data... Press ctrl+c to exit immediately.' + shutdown_thread.join + end + + nil + end + end + end +end diff --git a/lib/datadog/core/configuration/agent_settings_resolver.rb b/lib/datadog/core/configuration/agent_settings_resolver.rb new file mode 100644 index 00000000000..ebcd0ccc505 --- /dev/null +++ b/lib/datadog/core/configuration/agent_settings_resolver.rb @@ -0,0 +1,311 @@ +# typed: false +require 'uri' + +require 'ddtrace/ext/transport' +require 'datadog/core/configuration/settings' + +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. + # + # rubocop:disable Metrics/ClassLength + class AgentSettingsResolver + AgentSettings = \ + Struct.new( + :adapter, + :ssl, + :hostname, + :port, + :uds_path, + :timeout_seconds, + :deprecated_for_removal_transport_configuration_proc, + :deprecated_for_removal_transport_configuration_options + ) do + def initialize( + adapter:, + ssl:, + hostname:, + port:, + uds_path:, + timeout_seconds:, + deprecated_for_removal_transport_configuration_proc:, + deprecated_for_removal_transport_configuration_options: + ) + super( + adapter, + ssl, + hostname, + port, + uds_path, + timeout_seconds, + deprecated_for_removal_transport_configuration_proc, + deprecated_for_removal_transport_configuration_options, + ) + freeze + end + + # Returns a frozen copy of this struct + # with the provided +member_values+ modified. + def merge(**member_values) + new_struct = dup + + member_values.each do |member, value| + new_struct[member] = value + end + + new_struct.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 + 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, + deprecated_for_removal_transport_configuration_options: deprecated_for_removal_transport_configuration_options + ) + end + + def adapter + # 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. + if should_use_uds_fallback? + Ext::Transport::UnixSocket::ADAPTER + else + Ext::Transport::HTTP::ADAPTER + end + end + + def configured_hostname + return @configured_hostname if defined?(@configured_hostname) + + @configured_hostname = pick_from( + DetectedConfiguration.new( + friendly_name: "'c.tracer.hostname'", + value: settings.tracer.hostname + ), + DetectedConfiguration.new( + friendly_name: "#{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_URL} environment variable", + value: parsed_url && parsed_url.hostname + ), + DetectedConfiguration.new( + friendly_name: "#{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_HOST} environment variable", + value: ENV[Datadog::Ext::Transport::HTTP::ENV_DEFAULT_HOST] + ) + ) + end + + def configured_port + return @configured_port if defined?(@configured_port) + + parsed_port_from_env = + try_parsing_as_integer( + friendly_name: "#{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_PORT} environment variable", + value: ENV[Datadog::Ext::Transport::HTTP::ENV_DEFAULT_PORT], + ) + + parsed_settings_tracer_port = + try_parsing_as_integer( + friendly_name: '"c.tracer.port"', + value: settings.tracer.port, + ) + + @configured_port = pick_from( + DetectedConfiguration.new( + friendly_name: '"c.tracer.port"', + value: parsed_settings_tracer_port, + ), + DetectedConfiguration.new( + friendly_name: "#{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_URL} environment variable", + value: parsed_url && parsed_url.port, + ), + DetectedConfiguration.new( + friendly_name: "#{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_PORT} environment variable", + value: parsed_port_from_env, + ) + ) + end + + def try_parsing_as_integer(value:, friendly_name:) + return unless value + + begin + Integer(value) + rescue ArgumentError, TypeError + log_warning("Invalid value for #{friendly_name} (#{value.inspect}). Ignoring this configuration.") + + nil + end + end + + def ssl? + !parsed_url.nil? && parsed_url.scheme == 'https' + end + + def hostname + configured_hostname || (should_use_uds_fallback? ? nil : Datadog::Ext::Transport::HTTP::DEFAULT_HOST) + end + + def port + configured_port || (should_use_uds_fallback? ? nil : Datadog::Ext::Transport::HTTP::DEFAULT_PORT) + end + + # Unix socket path in the file system + def uds_path + uds_fallback + end + + # Defaults to +nil+, letting the adapter choose what default + # works best in their case. + def timeout_seconds + nil + end + + def deprecated_for_removal_transport_configuration_proc + settings.tracer.transport_options if settings.tracer.transport_options.is_a?(Proc) + end + + def deprecated_for_removal_transport_configuration_options + options = settings.tracer.transport_options + + if options.is_a?(Hash) && !options.empty? + log_warning( + 'Configuring the tracer via a c.tracer.transport_options hash is deprecated for removal in a future ' \ + "ddtrace version (c.tracer.transport_options contained '#{options.inspect}')." + ) + + options + 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? && + deprecated_for_removal_transport_configuration_options.nil? && + File.exist?(Ext::Transport::UnixSocket::DEFAULT_PATH) + + Ext::Transport::UnixSocket::DEFAULT_PATH + end + end + + def should_use_uds_fallback? + uds_fallback != nil + end + + def parsed_url + return @parsed_url if defined?(@parsed_url) + + @parsed_url = + if unparsed_url_from_env + parsed = URI.parse(unparsed_url_from_env) + + if %w[http https].include?(parsed.scheme) + parsed + else + log_warning( + "Invalid URI scheme '#{parsed.scheme}' for #{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_URL} " \ + "environment variable ('#{unparsed_url_from_env}'). " \ + "Ignoring the contents of #{Datadog::Ext::Transport::HTTP::ENV_DEFAULT_URL}." + ) + + nil + end + end + end + + # NOTE: This should only be used AFTER parsing, via `#parsed_url`. The only other use-case where this can be used + # directly without parsing, is when displaying in warning messages, to show users what it actually contains. + def unparsed_url_from_env + @unparsed_url_from_env ||= ENV[Datadog::Ext::Transport::HTTP::ENV_DEFAULT_URL] + 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}." + ) + end + + def log_warning(message) + logger.warn(message) if logger + end + + DetectedConfiguration = Struct.new(:friendly_name, :value) do + def initialize(friendly_name:, value:) + super(friendly_name, value) + freeze + end + + def value? + !value.nil? + end + end + private_constant :DetectedConfiguration + + # NOTE: Due to... legacy reasons... Some classes like having an `AgentSettings` instance to fall back to. + # Because we generate this instance with an empty instance of `Settings`, the resulting `AgentSettings` below + # represents only settings specified via environment variables + the usual defaults. + # + # YOU DO NOT WANT TO USE THE BELOW INSTANCE ON ANY NEWLY WRITTEN CODE, as it ignores any settings specified + # by users via `Datadog.configure`. + ENVIRONMENT_AGENT_SETTINGS = call(Settings.new, logger: nil) + end + # rubocop:enable Metrics/ClassLength + end + end +end diff --git a/lib/datadog/core/configuration/base.rb b/lib/datadog/core/configuration/base.rb new file mode 100644 index 00000000000..f4b1b33fbf2 --- /dev/null +++ b/lib/datadog/core/configuration/base.rb @@ -0,0 +1,88 @@ +# typed: false +require 'datadog/core/environment/variable_helpers' +require 'datadog/core/configuration/options' + +module Datadog + module Core + module Configuration + # Basic configuration behavior + # @public_api + module Base + def self.included(base) + base.extend(Core::Environment::VariableHelpers) + base.include(Core::Environment::VariableHelpers) + base.include(Options) + + base.extend(ClassMethods) + base.include(InstanceMethods) + end + + # Class methods for configuration + # @public_api + module ClassMethods + protected + + # Allows subgroupings of settings to be defined. + # e.g. `settings :foo { option :bar }` --> `config.foo.bar` + # @param [Symbol] name option name. Methods will be created based on this name. + def settings(name, &block) + settings_class = new_settings_class(&block) + + option(name) do |o| + o.default { settings_class.new } + o.lazy + o.resetter do |value| + value.reset! if value.respond_to?(:reset!) + value + end + end + end + + private + + def new_settings_class(&block) + Class.new { include Configuration::Base }.tap do |klass| + klass.instance_eval(&block) if block + end + end + end + + # Instance methods for configuration + # @public_api + module InstanceMethods + def initialize(options = {}) + configure(options) unless options.empty? + end + + def configure(opts = {}) + # Sort the options in preference of dependency order first + ordering = self.class.options.dependency_order + sorted_opts = opts.sort_by do |name, _value| + ordering.index(name) || (ordering.length + 1) + end.to_h + + # Apply options in sort order + sorted_opts.each do |name, value| + if respond_to?("#{name}=") + send("#{name}=", value) + elsif option_defined?(name) + set_option(name, value) + end + end + + # Apply any additional settings from block + yield(self) if block_given? + end + + def to_h + options_hash + end + + def reset! + reset_options! + end + end + end + end + end +end diff --git a/lib/datadog/core/configuration/components.rb b/lib/datadog/core/configuration/components.rb new file mode 100644 index 00000000000..ecf4705eeda --- /dev/null +++ b/lib/datadog/core/configuration/components.rb @@ -0,0 +1,381 @@ +# typed: false +require 'datadog/core/configuration/agent_settings_resolver' +require 'datadog/core/diagnostics/health' +require 'datadog/core/logger' +require 'ddtrace/profiling' +require 'datadog/core/runtime/metrics' +require 'ddtrace/tracer' +require 'ddtrace/trace_flush' +require 'ddtrace/sync_writer' +require 'datadog/core/workers/runtime_metrics' + +module Datadog + module Core + module Configuration + # Global components for the trace library. + # rubocop:disable Metrics/ClassLength + # rubocop:disable Layout/LineLength + class Components + class << self + def build_health_metrics(settings) + settings = settings.diagnostics.health_metrics + options = { enabled: settings.enabled } + options[:statsd] = settings.statsd unless settings.statsd.nil? + + Datadog::Core::Diagnostics::Health::Metrics.new(**options) + end + + def build_logger(settings) + logger = settings.logger.instance || Datadog::Core::Logger.new($stdout) + logger.level = settings.diagnostics.debug ? ::Logger::DEBUG : settings.logger.level + + logger + end + + def build_runtime_metrics(settings) + options = { enabled: settings.runtime_metrics.enabled } + options[:statsd] = settings.runtime_metrics.statsd unless settings.runtime_metrics.statsd.nil? + options[:services] = [settings.service] unless settings.service.nil? + + Datadog::Core::Runtime::Metrics.new(**options) + end + + def build_runtime_metrics_worker(settings) + # NOTE: Should we just ignore building the worker if its not enabled? + options = settings.runtime_metrics.opts.merge( + enabled: settings.runtime_metrics.enabled, + metrics: build_runtime_metrics(settings) + ) + + Datadog::Core::Workers::RuntimeMetrics.new(options) + end + + def build_tracer(settings, agent_settings) + # If a custom tracer has been provided, use it instead. + # Ignore all other options (they should already be configured.) + tracer = settings.tracer.instance + return tracer unless tracer.nil? + + # Apply test mode settings if test mode is activated + if settings.test_mode.enabled + trace_flush = build_test_mode_trace_flush(settings) + sampler = build_test_mode_sampler + writer = build_test_mode_writer(settings, agent_settings) + else + trace_flush = build_trace_flush(settings) + sampler = build_sampler(settings) + writer = build_writer(settings, agent_settings) + end + + subscribe_to_writer_events!(writer, sampler) + + Tracer.new( + default_service: settings.service, + enabled: settings.tracer.enabled, + trace_flush: trace_flush, + sampler: sampler, + writer: writer, + tags: build_tracer_tags(settings), + ) + end + + def build_trace_flush(settings) + if settings.tracer.partial_flush.enabled + Datadog::TraceFlush::Partial.new(min_spans_before_partial_flush: settings.tracer.partial_flush.min_spans_threshold) + else + Datadog::TraceFlush::Finished.new + end + end + + # TODO: Sampler should be a top-level component. + # It is currently part of the Tracer initialization + # process, but can take a variety of options (including + # a fully custom instance) that makes the Tracer + # initialization process complex. + def build_sampler(settings) + if (sampler = settings.tracer.sampler) + if settings.tracer.priority_sampling == false + sampler + else + ensure_priority_sampling(sampler, settings) + end + elsif settings.tracer.priority_sampling == false + Sampling::RuleSampler.new( + rate_limit: settings.sampling.rate_limit, + default_sample_rate: settings.sampling.default_rate + ) + else + PrioritySampler.new( + base_sampler: AllSampler.new, + post_sampler: Sampling::RuleSampler.new( + rate_limit: settings.sampling.rate_limit, + default_sample_rate: settings.sampling.default_rate + ) + ) + end + end + + def ensure_priority_sampling(sampler, settings) + if sampler.is_a?(PrioritySampler) + sampler + else + PrioritySampler.new( + base_sampler: sampler, + post_sampler: Sampling::RuleSampler.new( + rate_limit: settings.sampling.rate_limit, + default_sample_rate: settings.sampling.default_rate + ) + ) + end + end + + # TODO: Writer should be a top-level component. + # It is currently part of the Tracer initialization + # process, but can take a variety of options (including + # a fully custom instance) that makes the Tracer + # initialization process complex. + def build_writer(settings, agent_settings) + if (writer = settings.tracer.writer) + return writer + end + + Writer.new(agent_settings: agent_settings, **settings.tracer.writer_options) + end + + def subscribe_to_writer_events!(writer, sampler) + return unless writer.respond_to?(:events) # Check if it's a custom, external writer + + writer.events.after_send.subscribe(:record_environment_information, &WRITER_RECORD_ENVIRONMENT_INFORMATION_CALLBACK) + + return unless sampler.is_a?(Datadog::PrioritySampler) + + writer.events.after_send.subscribe( + :update_priority_sampler_rates, + &writer_update_priority_sampler_rates_callback(sampler) + ) + end + + WRITER_RECORD_ENVIRONMENT_INFORMATION_CALLBACK = lambda do |_, responses| + Core::Diagnostics::EnvironmentLogger.log!(responses) + end + + # Create new lambda for writer callback, + # capture the current sampler in the callback closure. + def writer_update_priority_sampler_rates_callback(sampler) + lambda do |_, responses| + response = responses.last + + next unless response && !response.internal_error? && response.service_rates + + sampler.update(response.service_rates) + end + end + + def build_profiler(settings, agent_settings, tracer) + return unless Datadog::Profiling.supported? && settings.profiling.enabled + + unless defined?(Datadog::Profiling::Tasks::Setup) + # In #1545 a user reported a NameError due to this constant being uninitialized + # I've documented my suspicion on why that happened in + # https://github.com/DataDog/dd-trace-rb/issues/1545#issuecomment-856049025 + # + # > Thanks for the info! It seems to feed into my theory: there's two moments in the code where we check if + # > profiler is "supported": 1) when loading ddtrace (inside preload) and 2) when starting the profile + # > after Datadog.configure gets run. + # > The problem is that the code assumes that both checks 1) and 2) will always reach the same conclusion: + # > either profiler is supported, or profiler is not supported. + # > In the problematic case, it looks like in your case check 1 decides that profiler is not + # > supported => doesn't load it, and then check 2 decides that it is => assumes it is loaded and tries to + # > start it. + # + # I was never able to validate if this was the issue or why exactly .supported? would change its mind BUT + # just in case it happens again, I've left this check which avoids breaking the user's application AND + # would instead direct them to report it to us instead, so that we can investigate what's wrong. + # + # TODO: As of June 2021, most checks in .supported? are related to the google-protobuf gem; so it's + # very likely that it was the origin of the issue we saw. Thus, if, as planned we end up moving away from + # protobuf OR enough time has passed and no users saw the issue again, we can remove this check altogether. + Datadog.logger.error( + 'Profiling was marked as supported and enabled, but setup task was not loaded properly. ' \ + 'Please report this at https://github.com/DataDog/dd-trace-rb/blob/master/CONTRIBUTING.md#found-a-bug' + ) + + return + end + + # Load extensions needed to support some of the Profiling features + Datadog::Profiling::Tasks::Setup.new.run + + # NOTE: Please update the Initialization section of ProfilingDevelopment.md with any changes to this method + + trace_identifiers_helper = Datadog::Profiling::TraceIdentifiers::Helper.new( + tracer: tracer, + endpoint_collection_enabled: settings.profiling.advanced.endpoint.collection.enabled + ) + + # TODO: It's a bit weird to treat this collector differently from others. See the TODO on the + # Datadog::Profiling::Recorder class for a discussion of this choice. + if settings.profiling.advanced.code_provenance_enabled + code_provenance_collector = + Datadog::Profiling::Collectors::CodeProvenance.new + end + + recorder = build_profiler_recorder(settings, code_provenance_collector) + collectors = build_profiler_collectors(settings, recorder, trace_identifiers_helper) + exporters = build_profiler_exporters(settings, agent_settings) + scheduler = build_profiler_scheduler(settings, recorder, exporters) + + Datadog::Profiler.new(collectors, scheduler) + end + + private + + def build_tracer_tags(settings) + settings.tags.dup.tap do |tags| + tags[Core::Environment::Ext::TAG_ENV] = settings.env unless settings.env.nil? + tags[Core::Environment::Ext::TAG_VERSION] = settings.version unless settings.version.nil? + end + end + + def build_test_mode_trace_flush(settings) + # If context flush behavior is provided, use it instead. + settings.test_mode.trace_flush || build_trace_flush(settings) + end + + def build_test_mode_sampler + # Do not sample any spans for tests; all must be preserved. + Datadog::AllSampler.new + end + + def build_test_mode_writer(settings, agent_settings) + # Flush traces synchronously, to guarantee they are written. + writer_options = settings.test_mode.writer_options || {} + Datadog::SyncWriter.new(agent_settings: agent_settings, **writer_options) + end + + def build_profiler_recorder(settings, code_provenance_collector) + event_classes = [Datadog::Profiling::Events::StackSample] + + Datadog::Profiling::Recorder.new( + event_classes, + settings.profiling.advanced.max_events, + code_provenance_collector: code_provenance_collector + ) + end + + def build_profiler_collectors(settings, recorder, trace_identifiers_helper) + [ + Datadog::Profiling::Collectors::Stack.new( + recorder, + trace_identifiers_helper: trace_identifiers_helper, + max_frames: settings.profiling.advanced.max_frames + # TODO: Provide proc that identifies Datadog worker threads? + # ignore_thread: settings.profiling.ignore_profiler + ) + ] + end + + def build_profiler_exporters(settings, agent_settings) + transport = + settings.profiling.exporter.transport || Datadog::Profiling::Transport::HTTP.default( + agent_settings: agent_settings, + site: settings.site, + api_key: settings.api_key, + profiling_upload_timeout_seconds: settings.profiling.upload.timeout_seconds + ) + + [Datadog::Profiling::Exporter.new(transport)] + end + + def build_profiler_scheduler(settings, recorder, exporters) + Datadog::Profiling::Scheduler.new(recorder, exporters) + end + end + + attr_reader \ + :health_metrics, + :logger, + :profiler, + :runtime_metrics, + :tracer + + def initialize(settings) + # Logger + @logger = self.class.build_logger(settings) + + agent_settings = AgentSettingsResolver.call(settings, logger: @logger) + + # Tracer + @tracer = self.class.build_tracer(settings, agent_settings) + + # Profiler + @profiler = self.class.build_profiler(settings, agent_settings, @tracer) + + # Runtime metrics + @runtime_metrics = self.class.build_runtime_metrics_worker(settings) + + # Health metrics + @health_metrics = self.class.build_health_metrics(settings) + end + + # Starts up components + def startup!(settings) + if settings.profiling.enabled + if profiler + @logger.debug('Profiling started') + profiler.start + else + # Display a warning for users who expected profiling to be enabled + unsupported_reason = Datadog::Profiling.unsupported_reason + logger.warn("Profiling was requested but is not supported, profiling disabled: #{unsupported_reason}") + end + else + @logger.debug('Profiling is disabled') + end + end + + # Shuts down all the components in use. + # If it has another instance to compare to, it will compare + # and avoid tearing down parts still in use. + def shutdown!(replacement = nil) + # Shutdown the old tracer, unless it's still being used. + # (e.g. a custom tracer instance passed in.) + tracer.shutdown! unless replacement && tracer == replacement.tracer + + # Shutdown old profiler + profiler.shutdown! unless profiler.nil? + + # Shutdown workers + runtime_metrics.stop(true, close_metrics: false) + + # Shutdown the old metrics, unless they are still being used. + # (e.g. custom Statsd instances.) + # + # TODO: This violates the encapsulation created by Runtime::Metrics and + # Health::Metrics, by directly manipulating `statsd` and changing + # it's lifecycle management. + # If we need to directly have ownership of `statsd` lifecycle, we should + # have direct ownership of it. + old_statsd = [ + runtime_metrics.metrics.statsd, + health_metrics.statsd + ].compact.uniq + + new_statsd = if replacement + [ + replacement.runtime_metrics.metrics.statsd, + replacement.health_metrics.statsd + ].compact.uniq + else + [] + end + + unused_statsd = (old_statsd - (old_statsd & new_statsd)) + unused_statsd.each(&:close) + end + end + # rubocop:enable Metrics/ClassLength + # rubocop:enable Layout/LineLength + end + end +end diff --git a/lib/datadog/core/configuration/dependency_resolver.rb b/lib/datadog/core/configuration/dependency_resolver.rb new file mode 100644 index 00000000000..52878108eb7 --- /dev/null +++ b/lib/datadog/core/configuration/dependency_resolver.rb @@ -0,0 +1,27 @@ +# typed: false +require 'tsort' + +module Datadog + module Core + module Configuration + # Resolver performs a topological sort over the dependency graph + class DependencyResolver + include TSort + + def initialize(dependency_graph = {}) + @dependency_graph = dependency_graph + end + + def tsort_each_node(&blk) + @dependency_graph.each_key(&blk) + end + + def tsort_each_child(node, &blk) + @dependency_graph.fetch(node).each(&blk) + end + + alias call tsort + end + end + end +end diff --git a/lib/datadog/core/configuration/option.rb b/lib/datadog/core/configuration/option.rb new file mode 100644 index 00000000000..8ca9e7d2e73 --- /dev/null +++ b/lib/datadog/core/configuration/option.rb @@ -0,0 +1,68 @@ +# typed: true +module Datadog + module Core + module Configuration + # Represents an instance of an integration configuration option + # @public_api + class Option + attr_reader \ + :definition + + def initialize(definition, context) + @definition = definition + @context = context + @value = nil + @is_set = false + end + + def set(value) + old_value = @value + (@value = context_exec(value, old_value, &definition.setter)).tap do |v| + @is_set = true + context_exec(v, old_value, &definition.on_set) if definition.on_set + end + end + + def get + if @is_set + @value + elsif definition.delegate_to + context_eval(&definition.delegate_to) + else + set(default_value) + end + end + + def reset + @value = if definition.resetter + # Don't change @is_set to false; custom resetters are + # responsible for changing @value back to a good state. + # Setting @is_set = false would cause a default to be applied. + context_exec(@value, &definition.resetter) + else + @is_set = false + nil + end + end + + def default_value + if definition.lazy + context_eval(&definition.default) + else + definition.default + end + end + + private + + def context_exec(*args, &block) + @context.instance_exec(*args, &block) + end + + def context_eval(&block) + @context.instance_eval(&block) + end + end + end + end +end diff --git a/lib/datadog/core/configuration/option_definition.rb b/lib/datadog/core/configuration/option_definition.rb new file mode 100644 index 00000000000..ad41d0aad31 --- /dev/null +++ b/lib/datadog/core/configuration/option_definition.rb @@ -0,0 +1,125 @@ +# typed: true +require 'datadog/core/configuration/option' + +module Datadog + module Core + module Configuration + # Represents a definition for an integration configuration option + class OptionDefinition + IDENTITY = ->(new_value, _old_value) { new_value } + + attr_reader \ + :default, + :delegate_to, + :depends_on, + :lazy, + :name, + :on_set, + :resetter, + :setter + + def initialize(name, meta = {}, &block) + @default = meta[:default] + @delegate_to = meta[:delegate_to] + @depends_on = meta[:depends_on] || [] + @lazy = meta[:lazy] || false + @name = name.to_sym + @on_set = meta[:on_set] + @resetter = meta[:resetter] + @setter = meta[:setter] || block || IDENTITY + end + + # Creates a new Option, bound to the context provided. + def build(context) + Option.new(self, context) + end + + # Acts as DSL for building OptionDefinitions + # @public_api + class Builder + attr_reader \ + :helpers + + def initialize(name, options = {}) + @default = nil + @delegate_to = nil + @depends_on = [] + @helpers = {} + @lazy = false + @name = name.to_sym + @on_set = nil + @resetter = nil + @setter = OptionDefinition::IDENTITY + + # If options were supplied, apply them. + apply_options!(options) + + # Apply block if given. + yield(self) if block_given? + end + + def depends_on(*values) + @depends_on = values.flatten + end + + def default(value = nil, &block) + @default = block || value + end + + def delegate_to(&block) + @delegate_to = block + end + + def helper(name, *_args, &block) + @helpers[name] = block + end + + def lazy(value = true) + @lazy = value + end + + def on_set(&block) + @on_set = block + end + + def resetter(&block) + @resetter = block + end + + def setter(&block) + @setter = block + end + + # For applying options for OptionDefinition + def apply_options!(options = {}) + return if options.nil? || options.empty? + + default(options[:default]) if options.key?(:default) + delegate_to(&options[:delegate_to]) if options.key?(:delegate_to) + depends_on(*options[:depends_on]) if options.key?(:depends_on) + lazy(options[:lazy]) if options.key?(:lazy) + on_set(&options[:on_set]) if options.key?(:on_set) + resetter(&options[:resetter]) if options.key?(:resetter) + setter(&options[:setter]) if options.key?(:setter) + end + + def to_definition + OptionDefinition.new(@name, meta) + end + + def meta + { + default: @default, + delegate_to: @delegate_to, + depends_on: @depends_on, + lazy: @lazy, + on_set: @on_set, + resetter: @resetter, + setter: @setter + } + end + end + end + end + end +end diff --git a/lib/datadog/core/configuration/option_definition_set.rb b/lib/datadog/core/configuration/option_definition_set.rb new file mode 100644 index 00000000000..dc7ec285392 --- /dev/null +++ b/lib/datadog/core/configuration/option_definition_set.rb @@ -0,0 +1,21 @@ +# typed: true +require 'datadog/core/configuration/dependency_resolver' + +module Datadog + module Core + module Configuration + # Represents a set of configuration option definitions for an integration + class OptionDefinitionSet < Hash + def dependency_order + DependencyResolver.new(dependency_graph).call + end + + def dependency_graph + each_with_object({}) do |(name, option), graph| + graph[name] = option.depends_on + end + end + end + end + end +end diff --git a/lib/datadog/core/configuration/option_set.rb b/lib/datadog/core/configuration/option_set.rb new file mode 100644 index 00000000000..2631068237b --- /dev/null +++ b/lib/datadog/core/configuration/option_set.rb @@ -0,0 +1,9 @@ +# typed: true +module Datadog + module Core + module Configuration + class OptionSet < Hash + end + end + end +end diff --git a/lib/datadog/core/configuration/options.rb b/lib/datadog/core/configuration/options.rb new file mode 100644 index 00000000000..5d0095942a7 --- /dev/null +++ b/lib/datadog/core/configuration/options.rb @@ -0,0 +1,117 @@ +# typed: false +require 'datadog/core/configuration/option_set' +require 'datadog/core/configuration/option_definition' +require 'datadog/core/configuration/option_definition_set' + +module Datadog + module Core + module Configuration + # Behavior for a configuration object that has options + # @public_api + module Options + def self.included(base) + base.extend(ClassMethods) + base.include(InstanceMethods) + end + + # Class behavior for a configuration object with options + # @public_api + module ClassMethods + def options + # Allows for class inheritance of option definitions + @options ||= superclass <= Options ? superclass.options.dup : OptionDefinitionSet.new + end + + protected + + def option(name, meta = {}, &block) + builder = OptionDefinition::Builder.new(name, meta, &block) + options[name] = builder.to_definition.tap do + # Resolve and define helper functions + helpers = default_helpers(name) + # Prevent unnecessary creation of an identical copy of helpers if there's nothing to merge + helpers = helpers.merge(builder.helpers) unless builder.helpers.empty? + define_helpers(helpers) + end + end + + private + + def default_helpers(name) + option_name = name.to_sym + + { + option_name.to_sym => proc do + get_option(option_name) + end, + "#{option_name}=".to_sym => proc do |value| + set_option(option_name, value) + end + } + end + + def define_helpers(helpers) + helpers.each do |name, block| + next unless block.is_a?(Proc) + + define_method(name, &block) + end + end + end + + # Instance behavior for a configuration object with options + # @public_api + module InstanceMethods + def options + @options ||= OptionSet.new + end + + def set_option(name, value) + add_option(name) unless options.key?(name) + options[name].set(value) + end + + def get_option(name) + add_option(name) unless options.key?(name) + options[name].get + end + + def reset_option(name) + assert_valid_option!(name) + options[name].reset if options.key?(name) + end + + def option_defined?(name) + self.class.options.key?(name) + end + + def options_hash + self.class.options.merge(options).each_with_object({}) do |(key, _), hash| + hash[key] = get_option(key) + end + end + + def reset_options! + options.values.each(&:reset) + end + + private + + def add_option(name) + assert_valid_option!(name) + definition = self.class.options[name] + definition.build(self).tap do |option| + options[name] = option + end + end + + def assert_valid_option!(name) + raise(InvalidOptionError, "#{self.class.name} doesn't define the option: #{name}") unless option_defined?(name) + end + end + + InvalidOptionError = Class.new(StandardError) + end + end + end +end diff --git a/lib/datadog/core/configuration/settings.rb b/lib/datadog/core/configuration/settings.rb new file mode 100644 index 00000000000..156a33a5d2d --- /dev/null +++ b/lib/datadog/core/configuration/settings.rb @@ -0,0 +1,619 @@ +# typed: false +require 'logger' +require 'datadog/core/configuration/base' + +require 'ddtrace/ext/analytics' +require 'ddtrace/ext/distributed' +require 'datadog/core/environment/ext' +require 'datadog/core/runtime/ext' +require 'ddtrace/ext/profiling' +require 'ddtrace/ext/sampling' +require 'ddtrace/ext/test' + +module Datadog + module Core + module Configuration + # Global configuration settings for the trace library. + # @public_api + # rubocop:disable Metrics/ClassLength + # rubocop:disable Layout/LineLength + class Settings + include Base + + # @!visibility private + def initialize(*_) + super + + # WORKAROUND: The values for services, version, and env can get set either directly OR as a side effect of + # accessing tags (reading or writing). This is of course really confusing and error-prone, e.g. in an app + # WITHOUT this workaround where you define `DD_TAGS=env:envenvtag,service:envservicetag,version:envversiontag` + # and do: + # + # puts Datadog.configuration.instance_exec { "#{service} #{env} #{version}" } + # Datadog.configuration.tags + # puts Datadog.configuration.instance_exec { "#{service} #{env} #{version}" } + # + # the output will be: + # + # [empty] + # envservicetag envenvtag envversiontag + # + # That is -- the proper values for service/env/version are only set AFTER something accidentally or not triggers + # the resolution of the tags. + # This is really confusing, error prone, etc, so calling tags here is a really hacky but effective way to + # avoid this. I could not think of a better way of fixing this issue without massive refactoring of tags parsing + # (so that the individual service/env/version get correctly set even from their tags values, not as a side + # effect). Sorry :( + tags + end + + # Legacy [App Analytics](https://docs.datadoghq.com/tracing/legacy_app_analytics/) configuration. + # + # @configure_with {Datadog::Tracing} + # @deprecated Use [Trace Retention and Ingestion](https://docs.datadoghq.com/tracing/trace_retention_and_ingestion/) + # controls. + # @public_api + settings :analytics do + # @default `DD_TRACE_ANALYTICS_ENABLED` environment variable, otherwise `nil` + # @return [Boolean,nil] + option :enabled do |o| + o.default { env_to_bool(Ext::Analytics::ENV_TRACE_ANALYTICS_ENABLED, nil) } + o.lazy + end + end + + # Datadog API key. + # + # For internal use only. + # + # @configure_with {Datadog} + # @default `DD_API_KEY` environment variable, otherwise `nil` + # @return [String,nil] + option :api_key do |o| + o.default { ENV.fetch(Core::Environment::Ext::ENV_API_KEY, nil) } + o.lazy + end + + # Datadog diagnostic settings. + # + # Enabling these surfaces debug information that can be helpful to + # diagnose issues related to Datadog internals. + # @configure_with {Datadog} + # @public_api + settings :diagnostics do + # Outputs all spans created by the host application to `Datadog.logger`. + # + # **This option is very verbose!** It's only recommended for non-production + # environments. + # + # This option is helpful when trying to understand what information the + # Datadog features are sending to the Agent or backend. + # @default `DD_TRACE_DEBUG` environment variable, otherwise `false` + # @return [Boolean] + option :debug do |o| + o.default { env_to_bool(Datadog::Core::Diagnostics::Ext::DD_TRACE_DEBUG, false) } + o.lazy + o.on_set do |enabled| + # Enable rich debug print statements. + # We do not need to unnecessarily load 'pp' unless in debugging mode. + require 'pp' if enabled + end + end + + # Internal {Datadog::Statsd} metrics collection. + # + # The list of metrics collected can be found in {Datadog::Core::Diagnostics::Ext::Health::Metrics}. + # @public_api + settings :health_metrics do + # Enable health metrics collection. + # + # @default `DD_HEALTH_METRICS_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :enabled do |o| + o.default { env_to_bool(Datadog::Core::Diagnostics::Ext::Health::Metrics::ENV_ENABLED, false) } + o.lazy + end + + # {Datadog::Statsd} instance to collect health metrics. + # + # If `nil`, health metrics creates a new {Datadog::Statsd} client with default agent configuration. + # + # @default `nil` + # @return [Datadog::Statsd,nil] a custom {Datadog::Statsd} instance + # @return [nil] an instance with default agent configuration will be lazily created + option :statsd + end + + # Tracer startup debug log statement configuration. + # @public_api + settings :startup_logs do + # Enable startup logs collection. + # + # If `nil`, defaults to logging startup logs when `ddtrace` detects that the application + # is *not* running in a development environment. + # + # @default `DD_TRACE_STARTUP_LOGS` environment variable, otherwise `nil` + # @return [Boolean,nil] + option :enabled do |o| + # Defaults to nil as we want to know when the default value is being used + o.default { env_to_bool(Datadog::Core::Diagnostics::Ext::DD_TRACE_STARTUP_LOGS, nil) } + o.lazy + end + end + end + + # [Distributed Tracing](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#distributed-tracing) propagation + # style configuration. + # + # The supported formats are: + # * `Datadog`: Datadog propagation format, described by [Distributed Tracing](https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#distributed-tracing). + # * `B3`: B3 Propagation using multiple headers, described by [openzipkin/b3-propagation](https://github.com/openzipkin/b3-propagation#multiple-headers). + # * `B3 single header`: B3 Propagation using a single header, described by [openzipkin/b3-propagation](https://github.com/openzipkin/b3-propagation#single-header). + # + # @configure_with {Datadog::Tracing} + # @public_api + settings :distributed_tracing do + # An ordered list of what data propagation styles the tracer will use to extract distributed tracing propagation + # data from incoming requests and messages. + # + # The tracer will try to find distributed headers in the order they are present in the list provided to this option. + # The first format to have valid data present will be used. + # + # @default `DD_PROPAGATION_STYLE_EXTRACT` environment variable (comma-separated list), + # otherwise `['Datadog','B3','B3 single header']`. + # @return [Array] + option :propagation_extract_style do |o| + o.default do + # Look for all headers by default + env_to_list( + Ext::DistributedTracing::PROPAGATION_STYLE_EXTRACT_ENV, + [ + Ext::DistributedTracing::PROPAGATION_STYLE_DATADOG, + Ext::DistributedTracing::PROPAGATION_STYLE_B3, + Ext::DistributedTracing::PROPAGATION_STYLE_B3_SINGLE_HEADER + ] + ) + end + + o.lazy + end + + # The data propagation styles the tracer will use to inject distributed tracing propagation + # data into outgoing requests and messages. + # + # The tracer will inject data from all styles specified in this option. + # + # @default `DD_PROPAGATION_STYLE_INJECT` environment variable (comma-separated list), otherwise `['Datadog']`. + # @return [Array] + option :propagation_inject_style do |o| + o.default do + env_to_list( + Ext::DistributedTracing::PROPAGATION_STYLE_INJECT_ENV, + [Ext::DistributedTracing::PROPAGATION_STYLE_DATADOG] # Only inject Datadog headers by default + ) + end + + o.lazy + end + end + + # The `env` tag in Datadog. Use it to separate out your staging, development, and production environments. + # @see https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging + # @default `DD_ENV` environment variable, otherwise `nil` + # @return [String,nil] + # @configure_with {Datadog} + option :env do |o| + # NOTE: env also gets set as a side effect of tags. See the WORKAROUND note in #initialize for details. + o.default { ENV.fetch(Core::Environment::Ext::ENV_ENVIRONMENT, nil) } + o.lazy + end + + # Automatic correlation between tracing and logging. + # @see https://docs.datadoghq.com/tracing/setup_overview/setup/ruby/#trace-correlation + # @return [Boolean] + # @configure_with {Datadog::Tracing} + option :log_injection do |o| + o.default { env_to_bool(Ext::Correlation::ENV_LOGS_INJECTION_ENABLED, true) } + o.lazy + end + + # Internal `Datadog.logger` configuration. + # + # This logger instance is only used internally by the gem. + # @configure_with {Datadog} + # @public_api + settings :logger do + # The `Datadog.logger` object. + # + # Can be overwritten with a custom logger object that respects the + # [built-in Ruby Logger](https://ruby-doc.org/stdlib-3.0.1/libdoc/logger/rdoc/Logger.html) + # interface. + # + # @return Logger::Severity + option :instance do |o| + o.on_set { |value| set_option(:level, value.level) unless value.nil? } + end + + # Log level for `Datadog.logger`. + # @see Logger::Severity + # @return Logger::Severity + option :level, default: ::Logger::INFO + end + + # Datadog Profiler-specific configurations. + # + # @see https://docs.datadoghq.com/tracing/profiler/ + # @configure_with {Datadog::Profiling} + # @public_api + settings :profiling do + # Enable profiling. + # + # @default `DD_PROFILING_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :enabled do |o| + o.default { env_to_bool(Ext::Profiling::ENV_ENABLED, false) } + o.lazy + end + + # @public_api + settings :exporter do + option :transport + end + + # @public_api + settings :advanced do + # This should never be reduced, as it can cause the resulting profiles to become biased. + # The current default should be enough for most services, allowing 16 threads to be sampled around 30 times + # per second for a 60 second period. + option :max_events, default: 32768 + + # Controls the maximum number of frames for each thread sampled. Can be tuned to avoid omitted frames in the + # produced profiles. Increasing this may increase the overhead of profiling. + option :max_frames do |o| + o.default { env_to_int(Ext::Profiling::ENV_MAX_FRAMES, 400) } + o.lazy + end + + # @public_api + settings :endpoint do + settings :collection do + # When using profiling together with tracing, this controls if endpoint names + # are gathered and reported together with profiles. + # + # @default `DD_PROFILING_ENDPOINT_COLLECTION_ENABLED` environment variable, otherwise `true` + # @return [Boolean] + option :enabled do |o| + o.default { env_to_bool(Ext::Profiling::ENV_ENDPOINT_COLLECTION_ENABLED, true) } + o.lazy + end + end + end + + # Disable gathering of names and versions of gems in use by the service, used to power grouping and + # categorization of stack traces. + option :code_provenance_enabled, default: true + end + + # @public_api + settings :upload do + option :timeout_seconds do |o| + o.setter { |value| value.nil? ? 30.0 : value.to_f } + o.default { env_to_float(Ext::Profiling::ENV_UPLOAD_TIMEOUT, 30.0) } + o.lazy + end + end + end + + option :report_hostname do |o| + o.default { env_to_bool(Ext::NET::ENV_REPORT_HOSTNAME, false) } + o.lazy + end + + # [Runtime Metrics](https://docs.datadoghq.com/tracing/runtime_metrics/) + # are StatsD metrics collected by the tracer to gain additional insights into an application's performance. + # @configure_with {Datadog} + # @public_api + settings :runtime_metrics do + # Enable runtime metrics. + # @default `DD_RUNTIME_METRICS_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :enabled do |o| + o.default { env_to_bool(Core::Runtime::Ext::Metrics::ENV_ENABLED, false) } + o.lazy + end + + option :opts, default: ->(_i) { {} }, lazy: true + option :statsd + end + + # Client-side sampling configuration. + # @configure_with {Datadog::Tracing} + # @public_api + settings :sampling do + # Default sampling rate for the tracer. + # + # If `nil`, the trace uses an automatic sampling strategy that tries to ensure + # the collection of traces that are considered important (e.g. traces with an error, traces + # for resources not seen recently). + # + # @default `DD_TRACE_SAMPLE_RATE` environment variable, otherwise `nil`. + # @return [Float,nil] + option :default_rate do |o| + o.default { env_to_float(Ext::Sampling::ENV_SAMPLE_RATE, nil) } + o.lazy + end + + # Rate limit for number of spans per second. + # + # Spans created above the limit will contribute to service metrics, but won't + # have their payload stored. + # + # @default `DD_TRACE_RATE_LIMIT` environment variable, otherwise 100. + # @return [Numeric,nil] + option :rate_limit do |o| + o.default { env_to_float(Ext::Sampling::ENV_RATE_LIMIT, 100) } + o.lazy + end + end + + # The `service` tag in Datadog. Use it to group related traces into a service. + # @see https://docs.datadoghq.com/getting_started/tagging/unified_service_tagging + # @default `DD_SERVICE` environment variable, otherwise the program name (e.g. `'ruby'`, `'rails'`, `'pry'`) + # @return [String] + # @configure_with {Datadog} + option :service do |o| + # NOTE: service also gets set as a side effect of tags. See the WORKAROUND note in #initialize for details. + o.default { ENV.fetch(Core::Environment::Ext::ENV_SERVICE, Core::Environment::Ext::FALLBACK_SERVICE_NAME) } + o.lazy + + # There's a few cases where we don't want to use the fallback service name, so this helper allows us to get a + # nil instead so that one can do + # nice_service_name = Datadog.configuration.service_without_fallback || nice_service_name_default + o.helper(:service_without_fallback) do + service_name = service + service_name unless service_name.equal?(Core::Environment::Ext::FALLBACK_SERVICE_NAME) + end + end + + # The Datadog site host to send data to. + # By default, data is sent to the Datadog US site: `app.datadoghq.com`. + # + # If your organization is on another site, you must update this value to the new site. + # + # For internal use only. + # + # @see https://docs.datadoghq.com/agent/troubleshooting/site/ + # @default `DD_SITE` environment variable, otherwise `nil` which sends data to `app.datadoghq.com` + # @return [String,nil] + # @configure_with {Datadog} + option :site do |o| + o.default { ENV.fetch(Core::Environment::Ext::ENV_SITE, nil) } + o.lazy + end + + # Default tags + # + # These tags are used by all Datadog products, when applicable. + # e.g. trace spans, profiles, etc. + # @default `DD_TAGS` environment variable (in the format `'tag1:value1,tag2:value2'`), otherwise `{}` + # @return [Hash] + # @configure_with {Datadog} + option :tags do |o| + o.default do + tags = {} + + # Parse tags from environment + env_to_list(Core::Environment::Ext::ENV_TAGS).each do |tag| + pair = tag.split(':') + tags[pair.first] = pair.last if pair.length == 2 + end + + # Override tags if defined + tags[Core::Environment::Ext::TAG_ENV] = env unless env.nil? + tags[Core::Environment::Ext::TAG_VERSION] = version unless version.nil? + + tags + end + + o.setter do |new_value, old_value| + # Coerce keys to strings + string_tags = new_value.collect { |k, v| [k.to_s, v] }.to_h + + # Cross-populate tag values with other settings + if env.nil? && string_tags.key?(Core::Environment::Ext::TAG_ENV) + self.env = string_tags[Core::Environment::Ext::TAG_ENV] + end + + if version.nil? && string_tags.key?(Core::Environment::Ext::TAG_VERSION) + self.version = string_tags[Core::Environment::Ext::TAG_VERSION] + end + + if service_without_fallback.nil? && string_tags.key?(Core::Environment::Ext::TAG_SERVICE) + self.service = string_tags[Core::Environment::Ext::TAG_SERVICE] + end + + # Merge with previous tags + (old_value || {}).merge(string_tags) + end + + o.lazy + end + + # [Continuous Integration Visibility](https://docs.datadoghq.com/continuous_integration/) configuration. + # @configure_with {Datadog::Tracing} + # @public_api + settings :test_mode do + # Enable test mode. This allows the tracer to collect spans from test runs. + # + # It also prevents the tracer from collecting spans in a production environment. Only use in a test environment. + # + # @default `DD_TRACE_TEST_MODE_ENABLED` environment variable, otherwise `false` + # @return [Boolean] + option :enabled do |o| + o.default { env_to_bool(Ext::Test::ENV_MODE_ENABLED, false) } + o.lazy + end + + option :trace_flush do |o| + o.default { nil } + o.lazy + end + + option :writer_options do |o| + o.default { {} } + o.lazy + end + end + + # The time provider used by Datadog. It must respect the interface of [Time](https://ruby-doc.org/core-3.0.1/Time.html). + # + # When testing, it can be helpful to use a different time provider. + # + # For [Timecop](https://rubygems.org/gems/timecop), for example, `->{ Time.now_without_mock_time }` + # allows Datadog features to use the real wall time when time is frozen. + # + # @default `->{ Time.now }` + # @return [Proc