datadog 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +47 -17
- data/ext/datadog_profiling_native_extension/extconf.rb +3 -8
- data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +3 -9
- data/ext/datadog_profiling_native_extension/profiling.c +6 -0
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +14 -4
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +0 -34
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/component.rb +1 -8
- data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
- data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
- data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
- data/lib/datadog/appsec/event.rb +1 -1
- data/lib/datadog/appsec/processor/context.rb +2 -2
- data/lib/datadog/appsec/remote.rb +1 -3
- data/lib/datadog/appsec/response.rb +7 -11
- data/lib/datadog/appsec.rb +3 -2
- data/lib/datadog/core/configuration/components.rb +17 -1
- data/lib/datadog/core/configuration/settings.rb +10 -0
- data/lib/datadog/core/configuration.rb +9 -1
- data/lib/datadog/core/remote/client/capabilities.rb +6 -0
- data/lib/datadog/core/remote/client.rb +65 -59
- data/lib/datadog/core/telemetry/component.rb +9 -3
- data/lib/datadog/core/telemetry/ext.rb +1 -0
- data/lib/datadog/di/code_tracker.rb +5 -4
- data/lib/datadog/di/component.rb +5 -1
- data/lib/datadog/di/contrib/active_record.rb +1 -0
- data/lib/datadog/di/init.rb +20 -0
- data/lib/datadog/di/instrumenter.rb +81 -11
- data/lib/datadog/di/probe.rb +11 -1
- data/lib/datadog/di/probe_builder.rb +1 -0
- data/lib/datadog/di/probe_manager.rb +4 -1
- data/lib/datadog/di/probe_notification_builder.rb +13 -7
- data/lib/datadog/di/remote.rb +124 -0
- data/lib/datadog/di/serializer.rb +14 -7
- data/lib/datadog/di/transport.rb +2 -1
- data/lib/datadog/di/utils.rb +7 -0
- data/lib/datadog/di.rb +84 -20
- data/lib/datadog/profiling/component.rb +4 -16
- data/lib/datadog/tracing/configuration/settings.rb +4 -8
- data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
- data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
- data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
- data/lib/datadog/tracing/tracer.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- data/lib/datadog.rb +3 -0
- metadata +17 -13
- data/lib/datadog/appsec/processor/actions.rb +0 -49
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Datadog
         | 
| 4 | 
            +
              module AppSec
         | 
| 5 | 
            +
                module Contrib
         | 
| 6 | 
            +
                  module ActiveRecord
         | 
| 7 | 
            +
                    # AppSec module that will be prepended to ActiveRecord adapter
         | 
| 8 | 
            +
                    module Instrumentation
         | 
| 9 | 
            +
                      module_function
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                      def detect_sql_injection(sql, adapter_name)
         | 
| 12 | 
            +
                        scope = AppSec.active_scope
         | 
| 13 | 
            +
                        return unless scope
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                        # libddwaf expects db system to be lowercase,
         | 
| 16 | 
            +
                        # in case of sqlite adapter, libddwaf expects 'sqlite' as db system
         | 
| 17 | 
            +
                        db_system = adapter_name.downcase
         | 
| 18 | 
            +
                        db_system = 'sqlite' if db_system == 'sqlite3'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                        ephemeral_data = {
         | 
| 21 | 
            +
                          'server.db.statement' => sql,
         | 
| 22 | 
            +
                          'server.db.system' => db_system
         | 
| 23 | 
            +
                        }
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                        waf_timeout = Datadog.configuration.appsec.waf_timeout
         | 
| 26 | 
            +
                        result = scope.processor_context.run({}, ephemeral_data, waf_timeout)
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                        if result.status == :match
         | 
| 29 | 
            +
                          Datadog::AppSec::Event.tag_and_keep!(scope, result)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                          event = {
         | 
| 32 | 
            +
                            waf_result: result,
         | 
| 33 | 
            +
                            trace: scope.trace,
         | 
| 34 | 
            +
                            span: scope.service_entry_span,
         | 
| 35 | 
            +
                            sql: sql,
         | 
| 36 | 
            +
                            actions: result.actions
         | 
| 37 | 
            +
                          }
         | 
| 38 | 
            +
                          scope.processor_context.events << event
         | 
| 39 | 
            +
                        end
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      # patch for all adapters in ActiveRecord >= 7.1
         | 
| 43 | 
            +
                      module InternalExecQueryAdapterPatch
         | 
| 44 | 
            +
                        def internal_exec_query(sql, *args, **rest)
         | 
| 45 | 
            +
                          Instrumentation.detect_sql_injection(sql, adapter_name)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                          super
         | 
| 48 | 
            +
                        end
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      # patch for postgres adapter in ActiveRecord < 7.1
         | 
| 52 | 
            +
                      module ExecuteAndClearAdapterPatch
         | 
| 53 | 
            +
                        def execute_and_clear(sql, *args, **rest)
         | 
| 54 | 
            +
                          Instrumentation.detect_sql_injection(sql, adapter_name)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                          super
         | 
| 57 | 
            +
                        end
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      # patch for mysql2 and sqlite3 adapters in ActiveRecord < 7.1
         | 
| 61 | 
            +
                      # this patch is also used when using JDBC adapter
         | 
| 62 | 
            +
                      module ExecQueryAdapterPatch
         | 
| 63 | 
            +
                        def exec_query(sql, *args, **rest)
         | 
| 64 | 
            +
                          Instrumentation.detect_sql_injection(sql, adapter_name)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                          super
         | 
| 67 | 
            +
                        end
         | 
| 68 | 
            +
                      end
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../integration'
         | 
| 4 | 
            +
            require_relative 'patcher'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Datadog
         | 
| 7 | 
            +
              module AppSec
         | 
| 8 | 
            +
                module Contrib
         | 
| 9 | 
            +
                  module ActiveRecord
         | 
| 10 | 
            +
                    # This class provides helper methods that are used when patching ActiveRecord
         | 
| 11 | 
            +
                    class Integration
         | 
| 12 | 
            +
                      include Datadog::AppSec::Contrib::Integration
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      MINIMUM_VERSION = Gem::Version.new('4')
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      register_as :active_record, auto_patch: false
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      def self.version
         | 
| 19 | 
            +
                        Gem.loaded_specs['activerecord'] && Gem.loaded_specs['activerecord'].version
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                      def self.loaded?
         | 
| 23 | 
            +
                        !defined?(::ActiveRecord).nil?
         | 
| 24 | 
            +
                      end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      def self.compatible?
         | 
| 27 | 
            +
                        super && version >= MINIMUM_VERSION
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      def self.auto_instrument?
         | 
| 31 | 
            +
                        true
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      def patcher
         | 
| 35 | 
            +
                        Patcher
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../patcher'
         | 
| 4 | 
            +
            require_relative 'instrumentation'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Datadog
         | 
| 7 | 
            +
              module AppSec
         | 
| 8 | 
            +
                module Contrib
         | 
| 9 | 
            +
                  module ActiveRecord
         | 
| 10 | 
            +
                    # AppSec patcher module for ActiveRecord
         | 
| 11 | 
            +
                    module Patcher
         | 
| 12 | 
            +
                      include Datadog::AppSec::Contrib::Patcher
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      module_function
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      def patched?
         | 
| 17 | 
            +
                        Patcher.instance_variable_get(:@patched)
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      def target_version
         | 
| 21 | 
            +
                        Integration.version
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      def patch
         | 
| 25 | 
            +
                        ActiveSupport.on_load :active_record do
         | 
| 26 | 
            +
                          instrumentation_module = if ::ActiveRecord.gem_version >= Gem::Version.new('7.1')
         | 
| 27 | 
            +
                                                     Instrumentation::InternalExecQueryAdapterPatch
         | 
| 28 | 
            +
                                                   else
         | 
| 29 | 
            +
                                                     Instrumentation::ExecQueryAdapterPatch
         | 
| 30 | 
            +
                                                   end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                          if defined?(::ActiveRecord::ConnectionAdapters::SQLite3Adapter)
         | 
| 33 | 
            +
                            ::ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(instrumentation_module)
         | 
| 34 | 
            +
                          end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                          if defined?(::ActiveRecord::ConnectionAdapters::Mysql2Adapter)
         | 
| 37 | 
            +
                            ::ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(instrumentation_module)
         | 
| 38 | 
            +
                          end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                          if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
         | 
| 41 | 
            +
                            unless defined?(::ActiveRecord::ConnectionAdapters::JdbcAdapter)
         | 
| 42 | 
            +
                              instrumentation_module = Instrumentation::ExecuteAndClearAdapterPatch
         | 
| 43 | 
            +
                            end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                            ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(instrumentation_module)
         | 
| 46 | 
            +
                          end
         | 
| 47 | 
            +
                        end
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
            end
         | 
    
        data/lib/datadog/appsec/event.rb
    CHANGED
    
    | @@ -142,7 +142,7 @@ module Datadog | |
| 142 142 | 
             
                      scope.trace.keep! if scope.trace
         | 
| 143 143 |  | 
| 144 144 | 
             
                      if scope.service_entry_span
         | 
| 145 | 
            -
                        scope.service_entry_span.set_tag('appsec.blocked', 'true') if waf_result.actions. | 
| 145 | 
            +
                        scope.service_entry_span.set_tag('appsec.blocked', 'true') if waf_result.actions.key?('block_request')
         | 
| 146 146 | 
             
                        scope.service_entry_span.set_tag('appsec.event', 'true')
         | 
| 147 147 | 
             
                      end
         | 
| 148 148 |  | 
| @@ -19,7 +19,7 @@ module Datadog | |
| 19 19 | 
             
                      @events = []
         | 
| 20 20 | 
             
                      @run_mutex = Mutex.new
         | 
| 21 21 |  | 
| 22 | 
            -
                      @libddwaf_debug_tag = "libddwaf:#{WAF::VERSION::STRING}"
         | 
| 22 | 
            +
                      @libddwaf_debug_tag = "libddwaf:#{WAF::VERSION::STRING} method:ddwaf_run"
         | 
| 23 23 | 
             
                    end
         | 
| 24 24 |  | 
| 25 25 | 
             
                    def run(persistent_data, ephemeral_data, timeout = WAF::LibDDWAF::DDWAF_RUN_TIMEOUT)
         | 
| @@ -79,7 +79,7 @@ module Datadog | |
| 79 79 | 
             
                      @context.run(persistent_data, ephemeral_data, timeout)
         | 
| 80 80 | 
             
                    rescue WAF::LibDDWAF::Error => e
         | 
| 81 81 | 
             
                      Datadog.logger.debug { "#{@libddwaf_debug_tag} execution error: #{e} backtrace: #{e.backtrace&.first(3)}" }
         | 
| 82 | 
            -
                      @telemetry.report(e, description: 'libddwaf internal low-level error')
         | 
| 82 | 
            +
                      @telemetry.report(e, description: 'libddwaf-rb internal low-level error')
         | 
| 83 83 |  | 
| 84 84 | 
             
                      [:err_internal, WAF::Result.new(:err_internal, [], 0.0, false, [], [])]
         | 
| 85 85 | 
             
                    end
         | 
| @@ -67,7 +67,6 @@ module Datadog | |
| 67 67 | 
             
                        data = []
         | 
| 68 68 | 
             
                        overrides = []
         | 
| 69 69 | 
             
                        exclusions = []
         | 
| 70 | 
            -
                        actions = []
         | 
| 71 70 |  | 
| 72 71 | 
             
                        repository.contents.each do |content|
         | 
| 73 72 | 
             
                          parsed_content = parse_content(content)
         | 
| @@ -81,7 +80,6 @@ module Datadog | |
| 81 80 | 
             
                            overrides << parsed_content['rules_override'] if parsed_content['rules_override']
         | 
| 82 81 | 
             
                            exclusions << parsed_content['exclusions'] if parsed_content['exclusions']
         | 
| 83 82 | 
             
                            custom_rules << parsed_content['custom_rules'] if parsed_content['custom_rules']
         | 
| 84 | 
            -
                            actions.concat(parsed_content['actions']) if parsed_content['actions']
         | 
| 85 83 | 
             
                          end
         | 
| 86 84 | 
             
                        end
         | 
| 87 85 |  | 
| @@ -105,7 +103,7 @@ module Datadog | |
| 105 103 | 
             
                          telemetry: telemetry
         | 
| 106 104 | 
             
                        )
         | 
| 107 105 |  | 
| 108 | 
            -
                        Datadog::AppSec.reconfigure(ruleset: ruleset,  | 
| 106 | 
            +
                        Datadog::AppSec.reconfigure(ruleset: ruleset, telemetry: telemetry)
         | 
| 109 107 | 
             
                      end
         | 
| 110 108 |  | 
| 111 109 | 
             
                      [receiver]
         | 
| @@ -31,19 +31,16 @@ module Datadog | |
| 31 31 | 
             
                    def negotiate(env, actions)
         | 
| 32 32 | 
             
                      # @type var configured_response: Response?
         | 
| 33 33 | 
             
                      configured_response = nil
         | 
| 34 | 
            -
                      actions.each do | | 
| 34 | 
            +
                      actions.each do |type, parameters|
         | 
| 35 35 | 
             
                        # Need to use next to make steep happy :(
         | 
| 36 36 | 
             
                        # I rather use break to stop the execution
         | 
| 37 37 | 
             
                        next if configured_response
         | 
| 38 38 |  | 
| 39 | 
            -
                         | 
| 40 | 
            -
                        next unless action_configuration
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                        configured_response = case action_configuration['type']
         | 
| 39 | 
            +
                        configured_response = case type
         | 
| 43 40 | 
             
                                              when 'block_request'
         | 
| 44 | 
            -
                                                block_response(env,  | 
| 41 | 
            +
                                                block_response(env, parameters)
         | 
| 45 42 | 
             
                                              when 'redirect_request'
         | 
| 46 | 
            -
                                                redirect_response(env,  | 
| 43 | 
            +
                                                redirect_response(env, parameters)
         | 
| 47 44 | 
             
                                              end
         | 
| 48 45 | 
             
                      end
         | 
| 49 46 |  | 
| @@ -90,7 +87,7 @@ module Datadog | |
| 90 87 | 
             
                      body << content(content_type)
         | 
| 91 88 |  | 
| 92 89 | 
             
                      Response.new(
         | 
| 93 | 
            -
                        status: options['status_code'] || 403,
         | 
| 90 | 
            +
                        status: options['status_code']&.to_i || 403,
         | 
| 94 91 | 
             
                        headers: { 'Content-Type' => content_type },
         | 
| 95 92 | 
             
                        body: body,
         | 
| 96 93 | 
             
                      )
         | 
| @@ -100,15 +97,14 @@ module Datadog | |
| 100 97 | 
             
                      if options['location'] && !options['location'].empty?
         | 
| 101 98 | 
             
                        content_type = content_type(env)
         | 
| 102 99 |  | 
| 103 | 
            -
                        status = options['status_code'] >= 300 && options['status_code'] < 400 ? options['status_code'] : 303
         | 
| 104 | 
            -
             | 
| 105 100 | 
             
                        headers = {
         | 
| 106 101 | 
             
                          'Content-Type' => content_type,
         | 
| 107 102 | 
             
                          'Location' => options['location']
         | 
| 108 103 | 
             
                        }
         | 
| 109 104 |  | 
| 105 | 
            +
                        status_code = options['status_code'].to_i
         | 
| 110 106 | 
             
                        Response.new(
         | 
| 111 | 
            -
                          status:  | 
| 107 | 
            +
                          status: (status_code >= 300 && status_code < 400 ? status_code : 303),
         | 
| 112 108 | 
             
                          headers: headers,
         | 
| 113 109 | 
             
                          body: [],
         | 
| 114 110 | 
             
                        )
         | 
    
        data/lib/datadog/appsec.rb
    CHANGED
    
    | @@ -24,12 +24,12 @@ module Datadog | |
| 24 24 | 
             
                    appsec_component.processor if appsec_component
         | 
| 25 25 | 
             
                  end
         | 
| 26 26 |  | 
| 27 | 
            -
                  def reconfigure(ruleset:,  | 
| 27 | 
            +
                  def reconfigure(ruleset:, telemetry:)
         | 
| 28 28 | 
             
                    appsec_component = components.appsec
         | 
| 29 29 |  | 
| 30 30 | 
             
                    return unless appsec_component
         | 
| 31 31 |  | 
| 32 | 
            -
                    appsec_component.reconfigure(ruleset: ruleset,  | 
| 32 | 
            +
                    appsec_component.reconfigure(ruleset: ruleset, telemetry: telemetry)
         | 
| 33 33 | 
             
                  end
         | 
| 34 34 |  | 
| 35 35 | 
             
                  def reconfigure_lock(&block)
         | 
| @@ -56,6 +56,7 @@ end | |
| 56 56 | 
             
            require_relative 'appsec/contrib/rack/integration'
         | 
| 57 57 | 
             
            require_relative 'appsec/contrib/sinatra/integration'
         | 
| 58 58 | 
             
            require_relative 'appsec/contrib/rails/integration'
         | 
| 59 | 
            +
            require_relative 'appsec/contrib/active_record/integration'
         | 
| 59 60 | 
             
            require_relative 'appsec/contrib/devise/integration'
         | 
| 60 61 | 
             
            require_relative 'appsec/contrib/graphql/integration'
         | 
| 61 62 |  | 
| @@ -13,6 +13,7 @@ require_relative '../remote/component' | |
| 13 13 | 
             
            require_relative '../../tracing/component'
         | 
| 14 14 | 
             
            require_relative '../../profiling/component'
         | 
| 15 15 | 
             
            require_relative '../../appsec/component'
         | 
| 16 | 
            +
            require_relative '../../di/component'
         | 
| 16 17 | 
             
            require_relative '../crashtracking/component'
         | 
| 17 18 |  | 
| 18 19 | 
             
            module Datadog
         | 
| @@ -83,6 +84,7 @@ module Datadog | |
| 83 84 | 
             
                      :telemetry,
         | 
| 84 85 | 
             
                      :tracer,
         | 
| 85 86 | 
             
                      :crashtracker,
         | 
| 87 | 
            +
                      :dynamic_instrumentation,
         | 
| 86 88 | 
             
                      :appsec
         | 
| 87 89 |  | 
| 88 90 | 
             
                    def initialize(settings)
         | 
| @@ -110,12 +112,13 @@ module Datadog | |
| 110 112 | 
             
                      @runtime_metrics = self.class.build_runtime_metrics_worker(settings)
         | 
| 111 113 | 
             
                      @health_metrics = self.class.build_health_metrics(settings)
         | 
| 112 114 | 
             
                      @appsec = Datadog::AppSec::Component.build_appsec_component(settings, telemetry: telemetry)
         | 
| 115 | 
            +
                      @dynamic_instrumentation = Datadog::DI::Component.build(settings, agent_settings, telemetry: telemetry)
         | 
| 113 116 |  | 
| 114 117 | 
             
                      self.class.configure_tracing(settings)
         | 
| 115 118 | 
             
                    end
         | 
| 116 119 |  | 
| 117 120 | 
             
                    # Starts up components
         | 
| 118 | 
            -
                    def startup!(settings)
         | 
| 121 | 
            +
                    def startup!(settings, old_state: nil)
         | 
| 119 122 | 
             
                      if settings.profiling.enabled
         | 
| 120 123 | 
             
                        if profiler
         | 
| 121 124 | 
             
                          profiler.start
         | 
| @@ -126,6 +129,16 @@ module Datadog | |
| 126 129 | 
             
                        end
         | 
| 127 130 | 
             
                      end
         | 
| 128 131 |  | 
| 132 | 
            +
                      if settings.remote.enabled && old_state&.[](:remote_started)
         | 
| 133 | 
            +
                        # The library was reconfigured and previously it already started
         | 
| 134 | 
            +
                        # the remote component (i.e., it received at least one request
         | 
| 135 | 
            +
                        # through the installed Rack middleware which started the remote).
         | 
| 136 | 
            +
                        # If the new configuration also has remote enabled, start the
         | 
| 137 | 
            +
                        # new remote right away.
         | 
| 138 | 
            +
                        # remote should always be not nil here but steep doesn't know this.
         | 
| 139 | 
            +
                        remote&.start
         | 
| 140 | 
            +
                      end
         | 
| 141 | 
            +
             | 
| 129 142 | 
             
                      Core::Diagnostics::EnvironmentLogger.collect_and_log!(@environment_logger_extra)
         | 
| 130 143 | 
             
                    end
         | 
| 131 144 |  | 
| @@ -136,6 +149,9 @@ module Datadog | |
| 136 149 | 
             
                      # Shutdown remote configuration
         | 
| 137 150 | 
             
                      remote.shutdown! if remote
         | 
| 138 151 |  | 
| 152 | 
            +
                      # Shutdown DI after remote, since remote config triggers DI operations.
         | 
| 153 | 
            +
                      dynamic_instrumentation&.shutdown!
         | 
| 154 | 
            +
             | 
| 139 155 | 
             
                      # Decommission AppSec
         | 
| 140 156 | 
             
                      appsec.shutdown! if appsec
         | 
| 141 157 |  | 
| @@ -863,6 +863,16 @@ module Datadog | |
| 863 863 | 
             
                        o.type :float
         | 
| 864 864 | 
             
                        o.default 1.0
         | 
| 865 865 | 
             
                      end
         | 
| 866 | 
            +
             | 
| 867 | 
            +
                      # Enable log collection for telemetry. Log collection only works when telemetry is enabled and
         | 
| 868 | 
            +
                      # logs are enabled.
         | 
| 869 | 
            +
                      # @default `DD_TELEMETRY_LOG_COLLECTION_ENABLED` environment variable, otherwise `true`.
         | 
| 870 | 
            +
                      # @return [Boolean]
         | 
| 871 | 
            +
                      option :log_collection_enabled do |o|
         | 
| 872 | 
            +
                        o.type :bool
         | 
| 873 | 
            +
                        o.env Core::Telemetry::Ext::ENV_LOG_COLLECTION
         | 
| 874 | 
            +
                        o.default true
         | 
| 875 | 
            +
                      end
         | 
| 866 876 | 
             
                    end
         | 
| 867 877 |  | 
| 868 878 | 
             
                    # Remote configuration
         | 
| @@ -258,8 +258,16 @@ module Datadog | |
| 258 258 | 
             
                  def replace_components!(settings, old)
         | 
| 259 259 | 
             
                    components = Components.new(settings)
         | 
| 260 260 |  | 
| 261 | 
            +
                    # Carry over state from existing components to the new ones.
         | 
| 262 | 
            +
                    # Currently, if we already started the remote component (which
         | 
| 263 | 
            +
                    # happens after a request goes through installed Rack middleware),
         | 
| 264 | 
            +
                    # we will start the new remote component as well.
         | 
| 265 | 
            +
                    old_state = {
         | 
| 266 | 
            +
                      remote_started: old.remote&.started?,
         | 
| 267 | 
            +
                    }
         | 
| 268 | 
            +
             | 
| 261 269 | 
             
                    old.shutdown!(components)
         | 
| 262 | 
            -
                    components.startup!(settings)
         | 
| 270 | 
            +
                    components.startup!(settings, old_state: old_state)
         | 
| 263 271 | 
             
                    components
         | 
| 264 272 | 
             
                  end
         | 
| 265 273 |  | 
| @@ -32,6 +32,12 @@ module Datadog | |
| 32 32 | 
             
                          register_receivers(Datadog::AppSec::Remote.receivers(@telemetry))
         | 
| 33 33 | 
             
                        end
         | 
| 34 34 |  | 
| 35 | 
            +
                        if settings.respond_to?(:dynamic_instrumentation) && settings.dynamic_instrumentation.enabled
         | 
| 36 | 
            +
                          register_capabilities(Datadog::DI::Remote.capabilities)
         | 
| 37 | 
            +
                          register_products(Datadog::DI::Remote.products)
         | 
| 38 | 
            +
                          register_receivers(Datadog::DI::Remote.receivers(@telemetry))
         | 
| 39 | 
            +
                        end
         | 
| 40 | 
            +
             | 
| 35 41 | 
             
                        register_capabilities(Datadog::Tracing::Remote.capabilities)
         | 
| 36 42 | 
             
                        register_products(Datadog::Tracing::Remote.products)
         | 
| 37 43 | 
             
                        register_receivers(Datadog::Tracing::Remote.receivers(@telemetry))
         | 
| @@ -24,93 +24,99 @@ module Datadog | |
| 24 24 | 
             
                      @dispatcher = Dispatcher.new(@capabilities.receivers)
         | 
| 25 25 | 
             
                    end
         | 
| 26 26 |  | 
| 27 | 
            -
                    # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity
         | 
| 28 27 | 
             
                    def sync
         | 
| 29 28 | 
             
                      # TODO: Skip sync if no capabilities are registered
         | 
| 30 29 | 
             
                      response = transport.send_config(payload)
         | 
| 31 30 |  | 
| 32 31 | 
             
                      if response.ok?
         | 
| 33 | 
            -
                         | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 32 | 
            +
                        process_response(response)
         | 
| 33 | 
            +
                      elsif response.internal_error?
         | 
| 34 | 
            +
                        raise TransportError, response.to_s
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
                    end
         | 
| 36 37 |  | 
| 37 | 
            -
             | 
| 38 | 
            -
                        end
         | 
| 38 | 
            +
                    private
         | 
| 39 39 |  | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 40 | 
            +
                    def process_response(response)
         | 
| 41 | 
            +
                      # when response is completely empty, do nothing as in: leave as is
         | 
| 42 | 
            +
                      if response.empty?
         | 
| 43 | 
            +
                        Datadog.logger.debug { 'remote: empty response => NOOP' }
         | 
| 44 44 |  | 
| 45 | 
            -
             | 
| 45 | 
            +
                        return
         | 
| 46 | 
            +
                      end
         | 
| 46 47 |  | 
| 47 | 
            -
             | 
| 48 | 
            -
                         | 
| 49 | 
            -
                           | 
| 48 | 
            +
                      begin
         | 
| 49 | 
            +
                        paths = response.client_configs.map do |path|
         | 
| 50 | 
            +
                          Configuration::Path.parse(path)
         | 
| 50 51 | 
             
                        end
         | 
| 51 52 |  | 
| 52 | 
            -
                         | 
| 53 | 
            -
             | 
| 53 | 
            +
                        targets = Configuration::TargetMap.parse(response.targets)
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                        contents = Configuration::ContentList.parse(response.target_files)
         | 
| 56 | 
            +
                      rescue Remote::Configuration::Path::ParseError => e
         | 
| 57 | 
            +
                        raise SyncError, e.message
         | 
| 58 | 
            +
                      end
         | 
| 54 59 |  | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 60 | 
            +
                      # To make sure steep does not complain
         | 
| 61 | 
            +
                      return unless paths && targets && contents
         | 
| 57 62 |  | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
                          (current.paths - paths).each { |p| transaction.delete(p) }
         | 
| 63 | 
            +
                      # TODO: sometimes it can strangely be so that paths.empty?
         | 
| 64 | 
            +
                      # TODO: sometimes it can strangely be so that targets.empty?
         | 
| 61 65 |  | 
| 62 | 
            -
             | 
| 63 | 
            -
             | 
| 64 | 
            -
                            # match target with path
         | 
| 65 | 
            -
                            target = targets[path]
         | 
| 66 | 
            +
                      apply_config(paths, targets, contents)
         | 
| 67 | 
            +
                    end
         | 
| 66 68 |  | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            +
                    def apply_config(paths, targets, contents)
         | 
| 70 | 
            +
                      changes = repository.transaction do |current, transaction|
         | 
| 71 | 
            +
                        # paths to be removed: previously applied paths minus ingress paths
         | 
| 72 | 
            +
                        (current.paths - paths).each { |p| transaction.delete(p) }
         | 
| 69 73 |  | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 74 | 
            +
                        # go through each ingress path
         | 
| 75 | 
            +
                        paths.each do |path|
         | 
| 76 | 
            +
                          # match target with path
         | 
| 77 | 
            +
                          target = targets[path]
         | 
| 72 78 |  | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
                            changed = current.paths.include?(path) && !current.contents.find_content(path, target)
         | 
| 79 | 
            +
                          # abort entirely if matching target not found
         | 
| 80 | 
            +
                          raise SyncError, "no target for path '#{path}'" if target.nil?
         | 
| 76 81 |  | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 82 | 
            +
                          # new paths are not in previously applied paths
         | 
| 83 | 
            +
                          new = !current.paths.include?(path)
         | 
| 79 84 |  | 
| 80 | 
            -
             | 
| 85 | 
            +
                          # updated paths are in previously applied paths
         | 
| 86 | 
            +
                          # but the content hash changed
         | 
| 87 | 
            +
                          changed = current.paths.include?(path) && !current.contents.find_content(path, target)
         | 
| 81 88 |  | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 89 | 
            +
                          # skip if unchanged
         | 
| 90 | 
            +
                          same = !new && !changed
         | 
| 84 91 |  | 
| 85 | 
            -
             | 
| 86 | 
            -
                            raise SyncError, "no valid content for target at path '#{path}'" if content.nil?
         | 
| 92 | 
            +
                          next if same
         | 
| 87 93 |  | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
                            transaction.insert(path, target, content) if new
         | 
| 91 | 
            -
                            transaction.update(path, target, content) if changed
         | 
| 92 | 
            -
                          end
         | 
| 94 | 
            +
                          # match content with path and target
         | 
| 95 | 
            +
                          content = contents.find_content(path, target)
         | 
| 93 96 |  | 
| 94 | 
            -
                          #  | 
| 95 | 
            -
                           | 
| 96 | 
            -
                          transaction.set(targets_version: targets.version)
         | 
| 97 | 
            +
                          # abort entirely if matching content not found
         | 
| 98 | 
            +
                          raise SyncError, "no valid content for target at path '#{path}'" if content.nil?
         | 
| 97 99 |  | 
| 98 | 
            -
                          #  | 
| 99 | 
            -
                          # TODO:  | 
| 100 | 
            +
                          # to be added or updated << config
         | 
| 101 | 
            +
                          # TODO: metadata (hash, version, etc...)
         | 
| 102 | 
            +
                          transaction.insert(path, target, content) if new
         | 
| 103 | 
            +
                          transaction.update(path, target, content) if changed
         | 
| 100 104 | 
             
                        end
         | 
| 101 105 |  | 
| 102 | 
            -
                         | 
| 103 | 
            -
             | 
| 104 | 
            -
                         | 
| 105 | 
            -
             | 
| 106 | 
            -
                        end
         | 
| 107 | 
            -
             | 
| 108 | 
            -
                        raise TransportError, response.to_s
         | 
| 106 | 
            +
                        # save backend opaque backend state
         | 
| 107 | 
            +
                        transaction.set(opaque_backend_state: targets.opaque_backend_state)
         | 
| 108 | 
            +
                        transaction.set(targets_version: targets.version)
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                        # upon transaction end, new list of applied config + metadata (add, change, remove) will be saved
         | 
| 111 | 
            +
                        # TODO: also remove stale config (matching removed) from cache (client configs is exhaustive list of paths)
         | 
| 109 112 | 
             
                      end
         | 
| 110 | 
            -
                    end
         | 
| 111 | 
            -
                    # rubocop:enable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/MethodLength,Metrics/CyclomaticComplexity
         | 
| 112 113 |  | 
| 113 | 
            -
             | 
| 114 | 
            +
                      if changes.empty?
         | 
| 115 | 
            +
                        Datadog.logger.debug { 'remote: no changes' }
         | 
| 116 | 
            +
                      else
         | 
| 117 | 
            +
                        dispatcher.dispatch(changes, repository)
         | 
| 118 | 
            +
                      end
         | 
| 119 | 
            +
                    end
         | 
| 114 120 |  | 
| 115 121 | 
             
                    def payload # rubocop:disable Metrics/MethodLength
         | 
| 116 122 | 
             
                      state = repository.state
         | 
| @@ -14,6 +14,7 @@ module Datadog | |
| 14 14 | 
             
              module Core
         | 
| 15 15 | 
             
                module Telemetry
         | 
| 16 16 | 
             
                  # Telemetry entrypoint, coordinates sending telemetry events at various points in app lifecycle.
         | 
| 17 | 
            +
                  # Note: Telemetry does not spawn its worker thread in fork processes, thus no telemetry is sent in forked processes.
         | 
| 17 18 | 
             
                  class Component
         | 
| 18 19 | 
             
                    attr_reader :enabled
         | 
| 19 20 |  | 
| @@ -52,6 +53,7 @@ module Datadog | |
| 52 53 | 
             
                        metrics_aggregation_interval_seconds: settings.telemetry.metrics_aggregation_interval_seconds,
         | 
| 53 54 | 
             
                        dependency_collection: settings.telemetry.dependency_collection,
         | 
| 54 55 | 
             
                        shutdown_timeout_seconds: settings.telemetry.shutdown_timeout_seconds,
         | 
| 56 | 
            +
                        log_collection_enabled: settings.telemetry.log_collection_enabled
         | 
| 55 57 | 
             
                      )
         | 
| 56 58 | 
             
                    end
         | 
| 57 59 |  | 
| @@ -67,10 +69,11 @@ module Datadog | |
| 67 69 | 
             
                      http_transport:,
         | 
| 68 70 | 
             
                      shutdown_timeout_seconds:,
         | 
| 69 71 | 
             
                      enabled: true,
         | 
| 70 | 
            -
                      metrics_enabled: true
         | 
| 72 | 
            +
                      metrics_enabled: true,
         | 
| 73 | 
            +
                      log_collection_enabled: true
         | 
| 71 74 | 
             
                    )
         | 
| 72 75 | 
             
                      @enabled = enabled
         | 
| 73 | 
            -
                      @ | 
| 76 | 
            +
                      @log_collection_enabled = log_collection_enabled
         | 
| 74 77 |  | 
| 75 78 | 
             
                      @metrics_manager = MetricsManager.new(
         | 
| 76 79 | 
             
                        enabled: enabled && metrics_enabled,
         | 
| @@ -86,6 +89,9 @@ module Datadog | |
| 86 89 | 
             
                        dependency_collection: dependency_collection,
         | 
| 87 90 | 
             
                        shutdown_timeout: shutdown_timeout_seconds
         | 
| 88 91 | 
             
                      )
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                      @stopped = false
         | 
| 94 | 
            +
             | 
| 89 95 | 
             
                      @worker.start
         | 
| 90 96 | 
             
                    end
         | 
| 91 97 |  | 
| @@ -114,7 +120,7 @@ module Datadog | |
| 114 120 | 
             
                    end
         | 
| 115 121 |  | 
| 116 122 | 
             
                    def log!(event)
         | 
| 117 | 
            -
                      return  | 
| 123 | 
            +
                      return if !@enabled || forked? || !@log_collection_enabled
         | 
| 118 124 |  | 
| 119 125 | 
             
                      @worker.enqueue(event)
         | 
| 120 126 | 
             
                    end
         | 
| @@ -13,6 +13,7 @@ module Datadog | |
| 13 13 | 
             
                    ENV_INSTALL_TYPE = 'DD_INSTRUMENTATION_INSTALL_TYPE'
         | 
| 14 14 | 
             
                    ENV_INSTALL_TIME = 'DD_INSTRUMENTATION_INSTALL_TIME'
         | 
| 15 15 | 
             
                    ENV_AGENTLESS_URL_OVERRIDE = 'DD_TELEMETRY_AGENTLESS_URL'
         | 
| 16 | 
            +
                    ENV_LOG_COLLECTION = 'DD_TELEMETRY_LOG_COLLECTION_ENABLED'
         | 
| 16 17 | 
             
                  end
         | 
| 17 18 | 
             
                end
         | 
| 18 19 | 
             
              end
         | 
| @@ -78,17 +78,18 @@ module Datadog | |
| 78 78 | 
             
                          registry_lock.synchronize do
         | 
| 79 79 | 
             
                            registry[path] = tp.instruction_sequence
         | 
| 80 80 | 
             
                          end
         | 
| 81 | 
            -
                        end
         | 
| 82 | 
            -
             | 
| 83 | 
            -
                        DI.component&.probe_manager&.install_pending_line_probes(path)
         | 
| 84 81 |  | 
| 82 | 
            +
                          # Also, pending line probes should only be installed for
         | 
| 83 | 
            +
                          # non-eval'd code.
         | 
| 84 | 
            +
                          DI.current_component&.probe_manager&.install_pending_line_probes(path)
         | 
| 85 | 
            +
                        end
         | 
| 85 86 | 
             
                      # Since this method normally is called from customer applications,
         | 
| 86 87 | 
             
                      # rescue any exceptions that might not be handled to not break said
         | 
| 87 88 | 
             
                      # customer applications.
         | 
| 88 89 | 
             
                      rescue => exc
         | 
| 89 90 | 
             
                        # TODO we do not have DI.component defined yet, remove steep:ignore
         | 
| 90 91 | 
             
                        # before release.
         | 
| 91 | 
            -
                        if component = DI. | 
| 92 | 
            +
                        if component = DI.current_component # steep:ignore
         | 
| 92 93 | 
             
                          raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
         | 
| 93 94 | 
             
                          component.logger.warn("Unhandled exception in script_compiled trace point: #{exc.class}: #{exc}")
         | 
| 94 95 | 
             
                          component.telemetry&.report(exc, description: "Unhandled exception in script_compiled trace point")
         |