datadog 2.26.0 → 2.27.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/ext/datadog_profiling_native_extension/clock_id_from_pthread.c +2 -1
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +7 -6
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +2 -2
  6. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +3 -2
  7. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -5
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +16 -12
  9. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +2 -2
  10. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +48 -1
  11. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +41 -0
  12. data/ext/datadog_profiling_native_extension/encoded_profile.c +2 -1
  13. data/ext/datadog_profiling_native_extension/heap_recorder.c +24 -24
  14. data/ext/datadog_profiling_native_extension/http_transport.c +10 -5
  15. data/ext/datadog_profiling_native_extension/libdatadog_helpers.c +3 -22
  16. data/ext/datadog_profiling_native_extension/libdatadog_helpers.h +0 -5
  17. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +9 -8
  18. data/ext/datadog_profiling_native_extension/profiling.c +20 -15
  19. data/ext/datadog_profiling_native_extension/ruby_helpers.c +55 -44
  20. data/ext/datadog_profiling_native_extension/ruby_helpers.h +17 -5
  21. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +8 -2
  22. data/ext/datadog_profiling_native_extension/setup_signal_handler.h +3 -0
  23. data/ext/datadog_profiling_native_extension/stack_recorder.c +16 -16
  24. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +2 -1
  25. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +5 -2
  26. data/ext/libdatadog_api/crashtracker.c +5 -8
  27. data/ext/libdatadog_api/datadog_ruby_common.c +48 -1
  28. data/ext/libdatadog_api/datadog_ruby_common.h +41 -0
  29. data/ext/libdatadog_api/ddsketch.c +4 -8
  30. data/ext/libdatadog_api/feature_flags.c +5 -5
  31. data/ext/libdatadog_api/helpers.h +27 -0
  32. data/ext/libdatadog_api/init.c +4 -0
  33. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +8 -1
  34. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +9 -2
  35. data/lib/datadog/appsec/component.rb +1 -1
  36. data/lib/datadog/appsec/context.rb +3 -3
  37. data/lib/datadog/appsec/contrib/excon/integration.rb +1 -1
  38. data/lib/datadog/appsec/contrib/excon/patcher.rb +1 -1
  39. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +47 -12
  40. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +32 -15
  41. data/lib/datadog/appsec/contrib/rest_client/integration.rb +1 -1
  42. data/lib/datadog/appsec/contrib/rest_client/patcher.rb +1 -1
  43. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +50 -14
  44. data/lib/datadog/appsec/ext.rb +2 -0
  45. data/lib/datadog/appsec/metrics/collector.rb +8 -3
  46. data/lib/datadog/appsec/metrics/exporter.rb +7 -0
  47. data/lib/datadog/appsec/metrics/telemetry.rb +7 -2
  48. data/lib/datadog/appsec/metrics.rb +5 -5
  49. data/lib/datadog/appsec/remote.rb +4 -4
  50. data/lib/datadog/appsec.rb +7 -1
  51. data/lib/datadog/core/configuration/settings.rb +17 -0
  52. data/lib/datadog/core/configuration/supported_configurations.rb +1 -0
  53. data/lib/datadog/core/telemetry/logger.rb +2 -0
  54. data/lib/datadog/core/telemetry/logging.rb +20 -2
  55. data/lib/datadog/profiling/component.rb +13 -0
  56. data/lib/datadog/profiling/exporter.rb +4 -0
  57. data/lib/datadog/profiling/ext/exec_monkey_patch.rb +32 -0
  58. data/lib/datadog/profiling/flush.rb +3 -0
  59. data/lib/datadog/profiling/profiler.rb +3 -5
  60. data/lib/datadog/profiling/scheduler.rb +8 -7
  61. data/lib/datadog/profiling/tag_builder.rb +1 -0
  62. data/lib/datadog/version.rb +1 -1
  63. metadata +6 -4
@@ -13,27 +13,62 @@ module Datadog
13
13
  # AppSec Middleware for Excon
14
14
  class SSRFDetectionMiddleware < ::Excon::Middleware::Base
15
15
  def request_call(data)
16
- return super unless AppSec.rasp_enabled? && AppSec.active_context
16
+ context = AppSec.active_context
17
+ return super unless context && AppSec.rasp_enabled?
18
+
19
+ timeout = Datadog.configuration.appsec.waf_timeout
20
+ ephemeral_data = {
21
+ 'server.io.net.url' => request_url(data),
22
+ 'server.io.net.request.method' => data[:method].to_s.upcase,
23
+ 'server.io.net.request.headers' => normalize_headers(data[:headers])
24
+ }
25
+
26
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_REQUEST_PHASE)
27
+ handle(result, context: context) if result.match?
28
+
29
+ super
30
+ end
17
31
 
32
+ def response_call(data)
18
33
  context = AppSec.active_context
34
+ return super unless context && AppSec.rasp_enabled?
19
35
 
20
- request_url = URI.join("#{data[:scheme]}://#{data[:host]}", data[:path]).to_s
21
- ephemeral_data = {'server.io.net.url' => request_url}
36
+ timeout = Datadog.configuration.appsec.waf_timeout
37
+ ephemeral_data = {
38
+ 'server.io.net.response.status' => data.dig(:response, :status).to_s,
39
+ 'server.io.net.response.headers' => normalize_headers(data.dig(:response, :headers))
40
+ }
22
41
 
23
- result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, Datadog.configuration.appsec.waf_timeout)
42
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_RESPONSE_PHASE)
43
+ handle(result, context: context) if result.match?
24
44
 
25
- if result.match?
26
- AppSec::Event.tag(context, result)
27
- TraceKeeper.keep!(context.trace) if result.keep?
45
+ super
46
+ end
28
47
 
29
- context.events.push(
30
- AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
31
- )
48
+ private
32
49
 
33
- AppSec::ActionsHandler.handle(result.actions)
50
+ def request_url(data)
51
+ klass = (data[:scheme] == 'https') ? URI::HTTPS : URI::HTTP
52
+ klass.build(host: data[:host], path: data[:path], query: data[:query]).to_s
53
+ end
54
+
55
+ def normalize_headers(headers)
56
+ return {} if headers.nil? || headers.empty?
57
+
58
+ headers.each_with_object({}) do |(key, value), memo|
59
+ memo[key.downcase] = value.is_a?(Array) ? value.join(', ') : value
34
60
  end
61
+ end
35
62
 
36
- super
63
+ def handle(result, context:)
64
+ AppSec::Event.tag(context, result)
65
+ TraceKeeper.keep!(context.trace) if result.keep?
66
+
67
+ context.events.push(
68
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
69
+ )
70
+
71
+ AppSec::ActionsHandler.handle(result.actions)
37
72
  end
38
73
  end
39
74
  end
@@ -10,33 +10,50 @@ module Datadog
10
10
  module Faraday
11
11
  # AppSec SSRF detection Middleware for Faraday
12
12
  class SSRFDetectionMiddleware < ::Faraday::Middleware
13
- def call(request_env)
13
+ def call(env)
14
14
  context = AppSec.active_context
15
+ return @app.call(env) unless context && AppSec.rasp_enabled?
15
16
 
16
- return @app.call(request_env) unless context && AppSec.rasp_enabled?
17
-
17
+ timeout = Datadog.configuration.appsec.waf_timeout
18
18
  ephemeral_data = {
19
- 'server.io.net.url' => request_env.url.to_s
19
+ 'server.io.net.url' => env.url.to_s,
20
+ 'server.io.net.request.method' => env.method.to_s.upcase,
21
+ 'server.io.net.request.headers' => env.request_headers.transform_keys(&:downcase)
20
22
  }
21
23
 
22
- result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, Datadog.configuration.appsec.waf_timeout)
24
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_REQUEST_PHASE)
25
+ handle(result, context: context) if result.match?
26
+
27
+ @app.call(env).on_complete { |response_env| on_complete(response_env, context: context) }
28
+ end
29
+
30
+ private
31
+
32
+ def on_complete(env, context:)
33
+ timeout = Datadog.configuration.appsec.waf_timeout
23
34
 
24
- if result.match?
25
- AppSec::Event.tag(context, result)
26
- TraceKeeper.keep!(context.trace) if result.keep?
35
+ response_headers = env.response_headers || {}
36
+ ephemeral_data = {
37
+ 'server.io.net.response.status' => env.status.to_s,
38
+ 'server.io.net.response.headers' => response_headers.transform_keys(&:downcase)
39
+ }
40
+
41
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_RESPONSE_PHASE)
42
+ handle(result, context: context) if result.match?
43
+ end
27
44
 
28
- context.events.push(
29
- AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
30
- )
45
+ def handle(result, context:)
46
+ AppSec::Event.tag(context, result)
47
+ TraceKeeper.keep!(context.trace) if result.keep?
31
48
 
32
- AppSec::ActionsHandler.handle(result.actions)
33
- end
49
+ context.events.push(
50
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
51
+ )
34
52
 
35
- @app.call(request_env)
53
+ AppSec::ActionsHandler.handle(result.actions)
36
54
  end
37
55
  end
38
56
  end
39
57
  end
40
58
  end
41
59
  end
42
- # rubocop:enable Naming/FileName
@@ -28,7 +28,7 @@ module Datadog
28
28
  end
29
29
 
30
30
  def self.compatible?
31
- super && version >= MINIMUM_VERSION
31
+ super && !!(version&.>= MINIMUM_VERSION)
32
32
  end
33
33
 
34
34
  def self.auto_instrument?
@@ -9,7 +9,7 @@ module Datadog
9
9
  module_function
10
10
 
11
11
  def patched?
12
- Patcher.instance_variable_get(:@patched)
12
+ !!Patcher.instance_variable_get(:@patched)
13
13
  end
14
14
 
15
15
  def target_version
@@ -11,29 +11,65 @@ module Datadog
11
11
  # Module that adds SSRF detection to RestClient::Request#execute
12
12
  module RequestSSRFDetectionPatch
13
13
  def execute(&block)
14
- return super unless AppSec.rasp_enabled? && AppSec.active_context
15
-
16
14
  context = AppSec.active_context
15
+ return super unless context && AppSec.rasp_enabled?
16
+
17
+ timeout = Datadog.configuration.appsec.waf_timeout
18
+ ephemeral_data = {
19
+ 'server.io.net.url' => url,
20
+ 'server.io.net.request.method' => method.to_s.upcase,
21
+ 'server.io.net.request.headers' => normalize_request_headers
22
+ }
23
+
24
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_REQUEST_PHASE)
25
+ handle(result, context: context) if result.match?
26
+
27
+ response = super
28
+
29
+ ephemeral_data = {
30
+ 'server.io.net.response.status' => response.code.to_s,
31
+ 'server.io.net.response.headers' => normalize_response_headers(response)
32
+ }
33
+
34
+ result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, timeout, phase: Ext::RASP_RESPONSE_PHASE)
35
+ handle(result, context: context) if result.match?
17
36
 
18
- ephemeral_data = {'server.io.net.url' => url}
19
- result = context.run_rasp(Ext::RASP_SSRF, {}, ephemeral_data, Datadog.configuration.appsec.waf_timeout)
37
+ response
38
+ end
39
+
40
+ private
41
+
42
+ # NOTE: Starting version 2.1.0 headers are already normalized via internal
43
+ # variable `@processed_headers_lowercase`. In case it's available,
44
+ # we use it to avoid unnecessary transformation.
45
+ def normalize_request_headers
46
+ return @processed_headers_lowercase if defined?(@processed_headers_lowercase)
20
47
 
21
- if result.match?
22
- AppSec::Event.tag(context, result)
23
- TraceKeeper.keep!(context.trace) if result.keep?
48
+ processed_headers.transform_keys(&:downcase)
49
+ end
50
+
51
+ # NOTE: Headers values are always an `Array` in `Net::HTTPResponse`,
52
+ # but we want to avoid accidents and will wrap them in no-op
53
+ # `Array` call just in case of a breaking change in the future
54
+ #
55
+ # FIXME: Steep has issues with `transform_values!` modifying the original
56
+ # type and it failed with "Cannot allow block body" error
57
+ def normalize_response_headers(response) # steep:ignore MethodBodyTypeMismatch
58
+ response.net_http_res.to_hash.transform_values! { |value| Array(value).join(', ') } # steep:ignore BlockBodyTypeMismatch
59
+ end
24
60
 
25
- context.events.push(
26
- AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
27
- )
61
+ def handle(result, context:)
62
+ AppSec::Event.tag(context, result)
63
+ TraceKeeper.keep!(context.trace) if result.keep?
28
64
 
29
- AppSec::ActionsHandler.handle(result.actions)
30
- end
65
+ context.events.push(
66
+ AppSec::SecurityEvent.new(result, trace: context.trace, span: context.span)
67
+ )
31
68
 
32
- super
69
+ AppSec::ActionsHandler.handle(result.actions)
33
70
  end
34
71
  end
35
72
  end
36
73
  end
37
74
  end
38
75
  end
39
- # rubocop:enable Naming/FileName
@@ -6,6 +6,8 @@ module Datadog
6
6
  RASP_SQLI = 'sql_injection'
7
7
  RASP_LFI = 'lfi'
8
8
  RASP_SSRF = 'ssrf'
9
+ RASP_REQUEST_PHASE = 'request'
10
+ RASP_RESPONSE_PHASE = 'response'
9
11
 
10
12
  PRODUCT_BIT = 0b00000010
11
13
 
@@ -13,6 +13,7 @@ module Datadog
13
13
  :duration_ns,
14
14
  :duration_ext_ns,
15
15
  :inputs_truncated,
16
+ :downstream_requests,
16
17
  keyword_init: true
17
18
  )
18
19
 
@@ -22,10 +23,13 @@ module Datadog
22
23
  @mutex = Mutex.new
23
24
 
24
25
  @waf = Store.new(
25
- evals: 0, matches: 0, errors: 0, timeouts: 0, duration_ns: 0, duration_ext_ns: 0, inputs_truncated: 0
26
+ evals: 0, matches: 0, errors: 0, timeouts: 0, duration_ns: 0,
27
+ duration_ext_ns: 0, inputs_truncated: 0, downstream_requests: 0
26
28
  )
29
+
27
30
  @rasp = Store.new(
28
- evals: 0, matches: 0, errors: 0, timeouts: 0, duration_ns: 0, duration_ext_ns: 0, inputs_truncated: 0
31
+ evals: 0, matches: 0, errors: 0, timeouts: 0, duration_ns: 0,
32
+ duration_ext_ns: 0, inputs_truncated: 0, downstream_requests: 0
29
33
  )
30
34
  end
31
35
 
@@ -41,7 +45,7 @@ module Datadog
41
45
  end
42
46
  end
43
47
 
44
- def record_rasp(result)
48
+ def record_rasp(result, type:, phase: nil)
45
49
  @mutex.synchronize do
46
50
  @rasp.evals += 1
47
51
  @waf.matches += 1 if result.match?
@@ -50,6 +54,7 @@ module Datadog
50
54
  @rasp.duration_ns += result.duration_ns
51
55
  @rasp.duration_ext_ns += result.duration_ext_ns
52
56
  @rasp.inputs_truncated += 1 if result.input_truncated?
57
+ @rasp.downstream_requests += 1 if type == Ext::RASP_SSRF && phase == Ext::RASP_REQUEST_PHASE
53
58
  end
54
59
  end
55
60
  end
@@ -22,6 +22,13 @@ module Datadog
22
22
  span.set_tag('_dd.appsec.rasp.timeout', 1) unless metrics.timeouts.zero?
23
23
  span.set_tag('_dd.appsec.rasp.duration', convert_ns_to_us(metrics.duration_ns))
24
24
  span.set_tag('_dd.appsec.rasp.duration_ext', convert_ns_to_us(metrics.duration_ext_ns))
25
+
26
+ # NOTE: In case of downstream requests being analyzed additionally
27
+ # with `Context.run_waf` method, we would need to share it
28
+ # between two exporting methods
29
+ unless metrics.downstream_requests.zero?
30
+ span.set_tag('_dd.appsec.downstream_request', metrics.downstream_requests)
31
+ end
25
32
  end
26
33
 
27
34
  # private
@@ -7,10 +7,15 @@ module Datadog
7
7
  module Telemetry
8
8
  module_function
9
9
 
10
- def report_rasp(type, result)
10
+ def report_rasp(type, result, phase: nil)
11
11
  return if result.error?
12
12
 
13
- tags = {rule_type: type, waf_version: Datadog::AppSec::WAF::VERSION::BASE_STRING}
13
+ tags = {rule_type: type, waf_version: WAF::VERSION::BASE_STRING}
14
+ tags[:rule_variant] = phase if phase
15
+
16
+ context = AppSec.active_context
17
+ tags[:event_rules_version] = context.waf_runner_ruleset_version if context
18
+
14
19
  namespace = Ext::TELEMETRY_METRICS_NAMESPACE
15
20
 
16
21
  AppSec.telemetry.inc(namespace, 'rasp.rule.eval', 1, tags: tags)
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'metrics/collector'
4
+ require_relative 'metrics/exporter'
5
+ require_relative 'metrics/telemetry'
6
+ require_relative 'metrics/telemetry_exporter'
7
+
3
8
  module Datadog
4
9
  module AppSec
5
10
  # This namespace contains classes related to metrics collection and exportation.
@@ -7,8 +12,3 @@ module Datadog
7
12
  end
8
13
  end
9
14
  end
10
-
11
- require_relative 'metrics/collector'
12
- require_relative 'metrics/exporter'
13
- require_relative 'metrics/telemetry'
14
- require_relative 'metrics/telemetry_exporter'
@@ -75,7 +75,8 @@ module Datadog
75
75
 
76
76
  matcher = Core::Remote::Dispatcher::Matcher::Product.new(ASM_PRODUCTS)
77
77
  receiver = Core::Remote::Dispatcher::Receiver.new(matcher) do |repository, changes|
78
- next unless AppSec.security_engine
78
+ engine = AppSec.security_engine
79
+ next unless engine
79
80
 
80
81
  changes.each do |change|
81
82
  content = repository[change.path]
@@ -84,11 +85,10 @@ module Datadog
84
85
  case change.type
85
86
  when :insert, :update
86
87
  # @type var content: Core::Remote::Configuration::Content
87
- AppSec.security_engine.add_or_update_config(parse_content(content), path: change.path.to_s)
88
-
88
+ engine.add_or_update_config(parse_content(content), path: change.path.to_s)
89
89
  content.applied
90
90
  when :delete
91
- AppSec.security_engine.remove_config_at_path(change.path.to_s)
91
+ engine.remove_config_at_path(change.path.to_s)
92
92
  end
93
93
  end
94
94
 
@@ -22,8 +22,14 @@ module Datadog
22
22
  Datadog::AppSec::Context.active
23
23
  end
24
24
 
25
+ # NOTE: This is a temporary workaround for type checking.
26
+ #
27
+ # We want to move from possible nil-component to the disabled-component
28
+ # on an initialization error. Technically, telemetry will be never
29
+ # used if AppSec was not able to initialize, so it's safe to assume
30
+ # that telemetry will never be used and will be nil at the same time.
25
31
  def telemetry
26
- components.appsec&.telemetry
32
+ components.appsec&.telemetry || components.telemetry
27
33
  end
28
34
 
29
35
  def security_engine
@@ -436,6 +436,23 @@ module Datadog
436
436
  o.default true
437
437
  end
438
438
 
439
+ # The profiler gathers data by sending `SIGPROF` unix signals to Ruby application threads.
440
+ #
441
+ # When using `Kernel#exec` on Linux, it can happen that a signal sent before calling `exec` arrives after
442
+ # the new process is running, causing it to fail with the `Profiling timer expired` error message.
443
+ # To avoid this, the profiler installs a monkey patch on `Kernel#exec` to stop profiling before actually
444
+ # calling `exec`.
445
+ # This monkey patch is available for Ruby 2.7+; let us know if you need it on earlier Rubies.
446
+ # For more details see https://github.com/DataDog/dd-trace-rb/issues/5101 .
447
+ #
448
+ # @default `DD_PROFILING_SHUTDOWN_ON_EXEC_ENABLED` environment variable as a boolean,
449
+ # otherwise `true`
450
+ option :shutdown_on_exec_enabled do |o|
451
+ o.env 'DD_PROFILING_SHUTDOWN_ON_EXEC_ENABLED'
452
+ o.type :bool
453
+ o.default true
454
+ end
455
+
439
456
  # Configures how much wall-time overhead the profiler targets. The profiler will dynamically adjust the
440
457
  # interval between samples it takes so as to try and maintain the property that it spends no longer than
441
458
  # this amount of wall-clock time profiling. For example, with the default value of 2%, the profiler will
@@ -81,6 +81,7 @@ module Datadog
81
81
  "DD_PROFILING_NO_SIGNALS_WORKAROUND_ENABLED",
82
82
  "DD_PROFILING_OVERHEAD_TARGET_PERCENTAGE",
83
83
  "DD_PROFILING_PREVIEW_OTEL_CONTEXT_ENABLED",
84
+ "DD_PROFILING_SHUTDOWN_ON_EXEC_ENABLED",
84
85
  "DD_PROFILING_SIGHANDLER_SAMPLING_ENABLED",
85
86
  "DD_PROFILING_SKIP_MYSQL2_CHECK",
86
87
  "DD_PROFILING_TIMELINE_ENABLED",
@@ -14,10 +14,12 @@ module Datadog
14
14
  # read: lib/datadog/core/telemetry/logging.rb
15
15
  module Logger
16
16
  class << self
17
+ # (see Datadog::Core::Telemetry::Logging#report)
17
18
  def report(exception, level: :error, description: nil)
18
19
  instance&.report(exception, level: level, description: description)
19
20
  end
20
21
 
22
+ # (see Datadog::Core::Telemetry::Logging#error)
21
23
  def error(description)
22
24
  instance&.error(description)
23
25
  end
@@ -45,11 +45,20 @@ module Datadog
45
45
  end
46
46
  end
47
47
 
48
+ # @param exception [Exception] The exception to report
49
+ # @param level [Symbol] The log level (:error, :warn, :info, :debug)
50
+ # @param description [String, nil] A low cardinality description, without dynamic data
48
51
  def report(exception, level: :error, description: nil)
49
52
  # Anonymous exceptions to be logged as <Class:0x00007f8b1c0b3b40>
50
- message = +"#{exception.class.name || exception.class.inspect}" # standard:disable Style/RedundantInterpolation
53
+ message = +(exception.class.name || exception.class.inspect)
51
54
 
52
- message << ": #{description}" if description
55
+ telemetry_message = message_for_telemetry(exception)
56
+
57
+ if description || telemetry_message
58
+ message << ':'
59
+ message << " #{description}" if description
60
+ message << " (#{telemetry_message})" if telemetry_message
61
+ end
53
62
 
54
63
  event = Event::Log.new(
55
64
  message: message,
@@ -65,6 +74,15 @@ module Datadog
65
74
 
66
75
  log!(event)
67
76
  end
77
+
78
+ private
79
+
80
+ # A constant string representing the exception
81
+ def message_for_telemetry(exception)
82
+ return unless exception.instance_variable_defined?(:@telemetry_message)
83
+
84
+ exception.instance_variable_get(:@telemetry_message)
85
+ end
68
86
  end
69
87
  end
70
88
  end
@@ -82,6 +82,10 @@ module Datadog
82
82
  Datadog::Profiling::Ext::DirMonkeyPatches.apply!
83
83
  end
84
84
 
85
+ if can_apply_exec_monkey_patch?(settings)
86
+ Datadog::Profiling::Ext::ExecMonkeyPatch.apply!
87
+ end
88
+
85
89
  [profiler, {profiling_enabled: true}]
86
90
  end
87
91
 
@@ -434,6 +438,15 @@ module Datadog
434
438
  settings.profiling.advanced.dir_interruption_workaround_enabled
435
439
  end
436
440
 
441
+ private_class_method def self.can_apply_exec_monkey_patch?(settings)
442
+ return false if RUBY_VERSION < "2.7"
443
+
444
+ # This file is 2.7+ only so we only require it here once we've checked the Ruby version
445
+ require "datadog/profiling/ext/exec_monkey_patch"
446
+
447
+ settings.profiling.advanced.shutdown_on_exec_enabled
448
+ end
449
+
437
450
  private_class_method def self.enable_gvl_profiling?(settings, logger)
438
451
  return false if RUBY_VERSION < "3.2"
439
452
 
@@ -70,6 +70,9 @@ module Datadog
70
70
 
71
71
  uncompressed_code_provenance = code_provenance_collector.refresh.generate_json if code_provenance_collector
72
72
 
73
+ process_tags = Datadog.configuration.experimental_propagate_process_tags_enabled ?
74
+ Core::Environment::Process.serialized : ''
75
+
73
76
  Flush.new(
74
77
  start: start,
75
78
  finish: finish,
@@ -80,6 +83,7 @@ module Datadog
80
83
  settings: Datadog.configuration,
81
84
  profile_seq: sequence_tracker.get_next,
82
85
  ).to_a,
86
+ process_tags: process_tags,
83
87
  internal_metadata: internal_metadata.merge(
84
88
  {
85
89
  worker_stats: worker_stats,
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Profiling
5
+ module Ext
6
+ # The profiler gathers data by sending `SIGPROF` unix signals to Ruby application threads.
7
+ #
8
+ # When using `Kernel#exec` on Linux, it can happen that a signal sent before calling `exec` arrives after
9
+ # the new process is running, causing it to fail with the `Profiling timer expired` error message.
10
+ # To avoid this, the profiler installs a monkey patch on `Kernel#exec` to stop profiling before actually
11
+ # calling `exec`.
12
+ # This monkey patch is available for Ruby 2.7+; let us know if you need it on earlier Rubies.
13
+ # For more details see https://github.com/DataDog/dd-trace-rb/issues/5101 .
14
+ module ExecMonkeyPatch
15
+ def self.apply!
16
+ ::Object.prepend(ObjectMonkeyPatch)
17
+
18
+ true
19
+ end
20
+
21
+ module ObjectMonkeyPatch
22
+ private
23
+
24
+ def exec(...)
25
+ Datadog.send(:components, allow_initialization: false)&.profiler&.shutdown!(report_last_profile: false)
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -13,6 +13,7 @@ module Datadog
13
13
  :code_provenance_file_name,
14
14
  :code_provenance_data,
15
15
  :tags_as_array,
16
+ :process_tags,
16
17
  :internal_metadata_json,
17
18
  :info_json
18
19
 
@@ -23,6 +24,7 @@ module Datadog
23
24
  code_provenance_file_name:,
24
25
  code_provenance_data:,
25
26
  tags_as_array:,
27
+ process_tags:,
26
28
  internal_metadata:,
27
29
  info_json:
28
30
  )
@@ -32,6 +34,7 @@ module Datadog
32
34
  @code_provenance_file_name = code_provenance_file_name
33
35
  @code_provenance_data = code_provenance_data
34
36
  @tags_as_array = tags_as_array
37
+ @process_tags = process_tags
35
38
  @internal_metadata_json = JSON.generate(internal_metadata)
36
39
  @info_json = info_json
37
40
  end
@@ -31,10 +31,11 @@ module Datadog
31
31
  scheduler.start(on_failure_proc: proc { component_failed(:scheduler) })
32
32
  end
33
33
 
34
- def shutdown!
34
+ def shutdown!(report_last_profile: true)
35
35
  Datadog.logger.debug("Shutting down profiler")
36
36
 
37
37
  stop_worker
38
+ scheduler.disable_reporting unless report_last_profile
38
39
  stop_scheduler
39
40
  end
40
41
 
@@ -57,11 +58,8 @@ module Datadog
57
58
  Datadog::Core::Telemetry::Logger
58
59
  .error("Detected issue with profiler (#{failed_component} component), stopping profiling")
59
60
 
60
- # We explicitly not stop the crash tracker in this situation, under the assumption that, if a component failed,
61
- # we're operating in a degraded state and crash tracking may still be helpful.
62
-
63
61
  if failed_component == :worker
64
- scheduler.mark_profiler_failed
62
+ scheduler.disable_reporting
65
63
  stop_scheduler
66
64
  elsif failed_component == :scheduler
67
65
  stop_worker
@@ -24,7 +24,7 @@ module Datadog
24
24
  attr_reader \
25
25
  :exporter,
26
26
  :transport,
27
- :profiler_failed
27
+ :reporting_disabled
28
28
 
29
29
  public
30
30
 
@@ -36,7 +36,7 @@ module Datadog
36
36
  )
37
37
  @exporter = exporter
38
38
  @transport = transport
39
- @profiler_failed = false
39
+ @reporting_disabled = false
40
40
  @stop_requested = false
41
41
 
42
42
  # Workers::Async::Thread settings
@@ -83,14 +83,15 @@ module Datadog
83
83
  true
84
84
  end
85
85
 
86
- # This is called by the Profiler class whenever an issue happened in the profiler. This makes sure that even
87
- # if there is data to be flushed, we don't try to flush it.
88
- def mark_profiler_failed
89
- @profiler_failed = true
86
+ # Called by the Profiler when it wants to prevent any further reporting,
87
+ # even if there is data to be flushed.
88
+ # Used when the profiler failed or we're otherwise in a hurry to shut down.
89
+ def disable_reporting
90
+ @reporting_disabled = true
90
91
  end
91
92
 
92
93
  def work_pending?
93
- !profiler_failed && exporter.can_flush? && (run_loop? || !stop_requested?)
94
+ !reporting_disabled && exporter.can_flush? && (run_loop? || !stop_requested?)
94
95
  end
95
96
 
96
97
  def reset_after_fork
@@ -50,6 +50,7 @@ module Datadog
50
50
  FORM_FIELD_TAG_PROFILER_VERSION => profiler_version,
51
51
  'profile_seq' => profile_seq.to_s,
52
52
  )
53
+
53
54
  user_tag_keys = settings.tags.keys
54
55
  hash.keep_if { |tag| user_tag_keys.include?(tag) || ALLOWED_TAGS.include?(tag) }
55
56
  Core::Utils.encode_tags(hash)
@@ -3,7 +3,7 @@
3
3
  module Datadog
4
4
  module VERSION
5
5
  MAJOR = 2
6
- MINOR = 26
6
+ MINOR = 27
7
7
  PATCH = 0
8
8
  PRE = nil
9
9
  BUILD = nil