From 41980db50b8d3fae7db2b27921ad1a832ebfaa09 Mon Sep 17 00:00:00 2001 From: Oleg Pudeyev Date: Mon, 16 Sep 2024 17:29:07 -0400 Subject: [PATCH] DEBUG-2334 Dynamic instrumentation configuration --- Steepfile | 1 + lib/datadog/di.rb | 14 ++ lib/datadog/di/configuration.rb | 11 ++ lib/datadog/di/configuration/settings.rb | 163 ++++++++++++++++++ lib/datadog/di/extensions.rb | 16 ++ sig/datadog/di.rbs | 4 + sig/datadog/di/configuration.rbs | 6 + sig/datadog/di/configuration/settings.rbs | 10 ++ sig/datadog/di/extensions.rbs | 7 + .../datadog/di/configuration/settings_spec.rb | 78 +++++++++ 10 files changed, 310 insertions(+) create mode 100644 lib/datadog/di.rb create mode 100644 lib/datadog/di/configuration.rb create mode 100644 lib/datadog/di/configuration/settings.rb create mode 100644 lib/datadog/di/extensions.rb create mode 100644 sig/datadog/di.rbs create mode 100644 sig/datadog/di/configuration.rbs create mode 100644 sig/datadog/di/configuration/settings.rbs create mode 100644 sig/datadog/di/extensions.rbs create mode 100644 spec/datadog/di/configuration/settings_spec.rb diff --git a/Steepfile b/Steepfile index 7842cc604cc..dfae78bba26 100644 --- a/Steepfile +++ b/Steepfile @@ -101,6 +101,7 @@ target :datadog do ignore 'lib/datadog/core/workers/polling.rb' ignore 'lib/datadog/core/workers/queue.rb' ignore 'lib/datadog/core/workers/runtime_metrics.rb' + ignore 'lib/datadog/di/configuration/settings.rb' ignore 'lib/datadog/kit/appsec/events.rb' # disabled because of https://github.com/soutaro/steep/issues/701 ignore 'lib/datadog/kit/identity.rb' # disabled because of https://github.com/soutaro/steep/issues/701 ignore 'lib/datadog/opentelemetry.rb' diff --git a/lib/datadog/di.rb b/lib/datadog/di.rb new file mode 100644 index 00000000000..59ad88c722a --- /dev/null +++ b/lib/datadog/di.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_relative 'di/configuration' +require_relative 'di/extensions' + +module Datadog + # Namespace for Datadog dynamic instrumentation. + # + # @api private + module DI + # Expose DI to global shared objects + Extensions.activate! + end +end diff --git a/lib/datadog/di/configuration.rb b/lib/datadog/di/configuration.rb new file mode 100644 index 00000000000..52d4d0185ec --- /dev/null +++ b/lib/datadog/di/configuration.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Datadog + module DI + # Configuration for DI + module Configuration + end + end +end + +require_relative "configuration/settings" diff --git a/lib/datadog/di/configuration/settings.rb b/lib/datadog/di/configuration/settings.rb new file mode 100644 index 00000000000..5bf6f83d441 --- /dev/null +++ b/lib/datadog/di/configuration/settings.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module Datadog + module DI + module Configuration + # Settings + module Settings + def self.extended(base) + base = base.singleton_class unless base.is_a?(Class) + add_settings!(base) + end + + def self.add_settings!(base) + base.class_eval do + # The setting has "internal" prefix to prevent it from being + # prematurely turned on by customers. + settings :dynamic_instrumentation do + option :enabled do |o| + o.type :bool + # The environment variable has an "internal" prefix so that + # any customers that have the "proper" environment variable + # turned on (i.e. DD_DYNAMIC_INSTRUMENTATION_ENABLED) + # do not enable Ruby DI until the latter is ready for + # customer testing. + o.env "DD_DYNAMIC_INSTRUMENTATION_ENABLED" + o.default false + end + + # This option instructs dynamic instrumentation to use + # untargeted trace points when installing line probes and + # code tracking is not active. + # WARNING: untargeted trace points carry a massive performance + # penalty for the entire file in which a line probe is placed. + # + # If this option is set to false, which is the default, + # dynamic instrumentation will add probes that reference + # unknown files to the list of pending probes, and when + # the respective files are loaded, the line probes will be + # installed using targeted trace points. If the file in + # question is already loaded when the probe is received + # (for example, it is in a third-party library loaded during + # application boot), and code tracking was not active when + # the file was loaded, such files will not be instrumentable + # via line probes. + # + # If this option is set to true + # + # activated, DI will in + # activated or because the files being targeted have beenIf true and code tracking is not enabled, dynamic instrumentation + # will use untargeted trace points. + # If false and code tracking is not enabled, dynamic + # instrumentation will not instrument any files loaded + # WARNING: these trace points will greatly degrade performance + # of all code in the instrumented files. + option :untargeted_trace_points do |o| + o.type :bool + o.default false + end + + # If true, all of the catch-all rescue blocks in DI + # will propagate the exceptions onward. + # WARNING: for internal Datadog use only - this will break + # the DI product and potentially the library in general in + # a multitude of ways, cause resource leakage, permanent + # performance decreases, etc. + option :propagate_all_exceptions do |o| + o.type :bool + o.default false + end + + # An array of variable and key names to redact in addition to + # the built-in list of identifiers. + # + # The names will be normalized by removing the following + # symbols: _, -, @, $, and then matched to the complete + # variable or key name while ignoring the case. + # For example, specifying pass_word will match password and + # PASSWORD, and specifying PASSWORD will match pass_word. + # Note that, while the at sign (@) is used in Ruby to refer + # to instance variables, it does not have any significance + # for this setting (and is removed before matching identifiers). + option :redacted_identifiers do |o| + o.env "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS" + o.env_parser do |value| + value&.split(",")&.map(&:strip) + end + + o.type :array + o.default [] + end + + # An array of class names, values of which will be redacted from + # dynamic instrumentation snapshots. Example: FooClass. + # If a name is suffixed by '*', it becomes a wildcard and + # instances of any class whose name begins with the specified + # prefix will be redacted (example: Foo*). + # + # The names must all be fully-qualified, if any prefix of a + # class name is configured to be redacted, the value will be + # subject to redaction. For example, if Foo* is in the + # redacted class name list, instances of Foo, FooBar, + # Foo::Bar are all subject to redaction, but Bar::Foo will + # not be subject to redaction. + # + # Leading double-colon is permitted but has no effect, + # because the names are always considered to be fully-qualified. + # For example, adding ::Foo to the list will redact instances + # of Foo. + # + # Trailing colons should not be used because they will trigger + # exact match behavior but Ruby class names do not have + # trailing colons. For example, Foo:: will not cause anything + # to be redacted. Use Foo::* to redact all classes under + # the Foo module. + option :redacted_type_names do |o| + o.env "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES" + o.env_parser do |value| + value&.split(",")&.map(&:strip) + end + + o.type :array + o.default [] + end + + # Maximum number of object or collection traversals that + # will be permitted when serializing captured values. + option :max_capture_depth do |o| + o.type :int + o.default 3 + end + + # Maximum number of collection (Array and Hash) elements + # that will be captured. Arrays and hashes that have more + # elements will be truncated to this many elements. + option :max_capture_collection_size do |o| + o.type :int + o.default 100 + end + + # Strings longer than this length will be truncated to this + # length in dynamic instrumentation snapshots. + # + # Note that while all values are stringified during + # serialization, only values which are originally instances + # of the String class are subject to this length limit. + option :max_capture_string_length do |o| + o.type :int + o.default 255 + end + + # Maximim number of attributes that will be captured for + # a single non-primitive value. + option :max_capture_attribute_count do |o| + o.type :int + o.default 20 + end + end + end + end + end + end + end +end diff --git a/lib/datadog/di/extensions.rb b/lib/datadog/di/extensions.rb new file mode 100644 index 00000000000..1cd96a5ae5c --- /dev/null +++ b/lib/datadog/di/extensions.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "../core/configuration" +require_relative "configuration" + +module Datadog + module DI + # Extends Datadog tracing with DI features + module Extensions + # Inject DI into global objects. + def self.activate! + Core::Configuration::Settings.extend(Configuration::Settings) + end + end + end +end diff --git a/sig/datadog/di.rbs b/sig/datadog/di.rbs new file mode 100644 index 00000000000..da9704ab847 --- /dev/null +++ b/sig/datadog/di.rbs @@ -0,0 +1,4 @@ +module Datadog + module DI + end +end diff --git a/sig/datadog/di/configuration.rbs b/sig/datadog/di/configuration.rbs new file mode 100644 index 00000000000..c993aa74e16 --- /dev/null +++ b/sig/datadog/di/configuration.rbs @@ -0,0 +1,6 @@ +module Datadog + module DI + module Configuration + end + end +end diff --git a/sig/datadog/di/configuration/settings.rbs b/sig/datadog/di/configuration/settings.rbs new file mode 100644 index 00000000000..a31058a58ce --- /dev/null +++ b/sig/datadog/di/configuration/settings.rbs @@ -0,0 +1,10 @@ +module Datadog + module DI + module Configuration + module Settings + def self.extended: (untyped base) -> untyped + def self.add_settings!: (untyped base) -> untyped + end + end + end +end diff --git a/sig/datadog/di/extensions.rbs b/sig/datadog/di/extensions.rbs new file mode 100644 index 00000000000..ac0a8530534 --- /dev/null +++ b/sig/datadog/di/extensions.rbs @@ -0,0 +1,7 @@ +module Datadog + module DI + module Extensions + def self.activate!: () -> untyped + end + end +end diff --git a/spec/datadog/di/configuration/settings_spec.rb b/spec/datadog/di/configuration/settings_spec.rb new file mode 100644 index 00000000000..e0f5b75e3fb --- /dev/null +++ b/spec/datadog/di/configuration/settings_spec.rb @@ -0,0 +1,78 @@ +require "datadog/di" + +RSpec.describe Datadog::DI::Configuration::Settings do + subject(:settings) { Datadog::Core::Configuration::Settings.new } + + describe "dynamic_instrumentation" do + context "programmatic configuration" do + [ + ["enabled", true], + ["enabled", false], + ["untargeted_trace_points", true], + ["untargeted_trace_points", false], + ["propagate_all_exceptions", true], + ["propagate_all_exceptions", false], + ["redacted_identifiers", ["foo"]], + ["redacted_identifiers", []], + ["redacted_type_names", ["foo*", "bar"]], + ["redacted_type_names", []], + ["max_capture_depth", 5], + ["max_capture_collection_size", 10], + ["max_capture_string_length", 20], + ["max_capture_attribute_count", 4], + ].each do |(name_, value_)| + name = name_ + value = value_.freeze + + context "when #{name} set to #{value}" do + let(:value) { _value } + + before do + settings.dynamic_instrumentation.public_send("#{name}=", value) + end + + it "returns the value back" do + expect(settings.dynamic_instrumentation.public_send(name)).to eq(value) + end + end + end + end + + context "environment variable configuration" do + [ + ["DD_DYNAMIC_INSTRUMENTATION_ENABLED", "true", "enabled", true], + ["DD_DYNAMIC_INSTRUMENTATION_ENABLED", "false", "enabled", false], + ["DD_DYNAMIC_INSTRUMENTATION_ENABLED", nil, "enabled", false], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "foo", "redacted_identifiers", %w[foo]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "foo,bar", "redacted_identifiers", %w[foo bar]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "foo, bar", "redacted_identifiers", %w[foo bar]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "", "redacted_identifiers", %w[]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", ",", "redacted_identifiers", %w[]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS", "~?", "redacted_identifiers", %w[~?]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", "foo", "redacted_type_names", %w[foo]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", "foo,bar", "redacted_type_names", %w[foo bar]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", "foo, bar", "redacted_type_names", %w[foo bar]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", "", "redacted_type_names", %w[]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", ",", "redacted_type_names", %w[]], + ["DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES", ".!", "redacted_type_names", %w[.!]], + ].each do |(env_var_name_, env_var_value_, setting_name_, setting_value_)| + env_var_name = env_var_name_ + env_var_value = env_var_value_ + setting_name = setting_name_ + setting_value = setting_value_ + + context "when #{env_var_name}=#{env_var_value}" do + around do |example| + ClimateControl.modify(env_var_name => env_var_value) do + example.run + end + end + + it "sets dynamic_instrumentation.#{setting_name}=#{setting_value}" do + expect(settings.dynamic_instrumentation.public_send(setting_name)).to eq setting_value + end + end + end + end + end +end