datadog 2.27.0 → 2.29.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -2
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -3
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +23 -4
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +5 -0
  7. data/ext/datadog_profiling_native_extension/heap_recorder.c +183 -51
  8. data/ext/datadog_profiling_native_extension/heap_recorder.h +12 -1
  9. data/ext/datadog_profiling_native_extension/stack_recorder.c +34 -5
  10. data/ext/datadog_profiling_native_extension/stack_recorder.h +2 -1
  11. data/ext/libdatadog_api/crashtracker.c +5 -0
  12. data/ext/libdatadog_api/crashtracker_report_exception.c +236 -0
  13. data/lib/datadog/ai_guard/configuration/settings.rb +13 -1
  14. data/lib/datadog/ai_guard/contrib/integration.rb +37 -0
  15. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +42 -0
  16. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +41 -0
  17. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +30 -0
  18. data/lib/datadog/ai_guard.rb +2 -0
  19. data/lib/datadog/appsec/assets/blocked.html +2 -1
  20. data/lib/datadog/appsec/configuration/settings.rb +14 -0
  21. data/lib/datadog/appsec/context.rb +44 -9
  22. data/lib/datadog/appsec/contrib/active_record/integration.rb +1 -1
  23. data/lib/datadog/appsec/contrib/active_record/patcher.rb +1 -1
  24. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +54 -5
  25. data/lib/datadog/appsec/contrib/faraday/integration.rb +1 -1
  26. data/lib/datadog/appsec/contrib/faraday/patcher.rb +1 -1
  27. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +60 -7
  28. data/lib/datadog/appsec/contrib/graphql/gateway/multiplex.rb +11 -6
  29. data/lib/datadog/appsec/contrib/graphql/integration.rb +1 -1
  30. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +6 -10
  31. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +1 -3
  32. data/lib/datadog/appsec/contrib/rails/patcher.rb +10 -2
  33. data/lib/datadog/appsec/contrib/rails/patches/process_action_patch.rb +2 -0
  34. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +72 -7
  35. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +7 -4
  36. data/lib/datadog/appsec/contrib/sinatra/integration.rb +1 -1
  37. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +4 -4
  38. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +1 -1
  39. data/lib/datadog/appsec/counter_sampler.rb +25 -0
  40. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +18 -0
  41. data/lib/datadog/appsec/security_engine/engine.rb +23 -2
  42. data/lib/datadog/appsec/utils/http/body.rb +38 -0
  43. data/lib/datadog/appsec/utils/http/media_range.rb +2 -1
  44. data/lib/datadog/appsec/utils/http/media_type.rb +33 -26
  45. data/lib/datadog/appsec/utils/http/url_encoded.rb +52 -0
  46. data/lib/datadog/core/configuration/components.rb +29 -4
  47. data/lib/datadog/core/configuration/supported_configurations.rb +4 -0
  48. data/lib/datadog/core/configuration.rb +2 -2
  49. data/lib/datadog/core/crashtracking/component.rb +79 -19
  50. data/lib/datadog/core/crashtracking/tag_builder.rb +6 -0
  51. data/lib/datadog/core/environment/agent_info.rb +65 -1
  52. data/lib/datadog/core/knuth_sampler.rb +57 -0
  53. data/lib/datadog/core/logger.rb +1 -1
  54. data/lib/datadog/core/metrics/logging.rb +1 -1
  55. data/lib/datadog/core/process_discovery.rb +15 -19
  56. data/lib/datadog/core/rate_limiter.rb +2 -0
  57. data/lib/datadog/core/remote/component.rb +16 -5
  58. data/lib/datadog/core/remote/transport/config.rb +5 -11
  59. data/lib/datadog/core/telemetry/component.rb +0 -13
  60. data/lib/datadog/core/telemetry/transport/telemetry.rb +5 -6
  61. data/lib/datadog/core/transport/ext.rb +1 -0
  62. data/lib/datadog/core/transport/http/response.rb +4 -0
  63. data/lib/datadog/core/transport/parcel.rb +61 -9
  64. data/lib/datadog/core/utils/fnv.rb +26 -0
  65. data/lib/datadog/core.rb +6 -1
  66. data/lib/datadog/data_streams/processor.rb +34 -33
  67. data/lib/datadog/data_streams/transport/http/stats.rb +6 -0
  68. data/lib/datadog/data_streams/transport/http.rb +0 -4
  69. data/lib/datadog/data_streams/transport/stats.rb +5 -12
  70. data/lib/datadog/di/component.rb +1 -1
  71. data/lib/datadog/di/configuration/settings.rb +31 -0
  72. data/lib/datadog/di/context.rb +6 -0
  73. data/lib/datadog/di/instrumenter.rb +178 -133
  74. data/lib/datadog/di/probe.rb +10 -1
  75. data/lib/datadog/di/probe_file_loader.rb +2 -2
  76. data/lib/datadog/di/probe_manager.rb +7 -2
  77. data/lib/datadog/di/probe_notification_builder.rb +29 -8
  78. data/lib/datadog/di/probe_notifier_worker.rb +13 -3
  79. data/lib/datadog/di/proc_responder.rb +4 -0
  80. data/lib/datadog/di/redactor.rb +8 -1
  81. data/lib/datadog/di/remote.rb +2 -2
  82. data/lib/datadog/di/transport/diagnostics.rb +5 -7
  83. data/lib/datadog/di/transport/http/diagnostics.rb +3 -1
  84. data/lib/datadog/di/transport/http/input.rb +1 -1
  85. data/lib/datadog/di/transport/input.rb +5 -6
  86. data/lib/datadog/kit/tracing/method_tracer.rb +132 -0
  87. data/lib/datadog/open_feature/transport.rb +8 -11
  88. data/lib/datadog/profiling/component.rb +0 -6
  89. data/lib/datadog/tracing/contrib/http/integration.rb +0 -2
  90. data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +6 -0
  91. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
  92. data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +6 -0
  93. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +2 -1
  94. data/lib/datadog/tracing/contrib/propagation/sql_comment/ext.rb +10 -0
  95. data/lib/datadog/tracing/contrib/propagation/sql_comment/mode.rb +5 -1
  96. data/lib/datadog/tracing/contrib/propagation/sql_comment.rb +24 -0
  97. data/lib/datadog/tracing/contrib/rack/route_inference.rb +18 -6
  98. data/lib/datadog/tracing/contrib/registerable.rb +11 -0
  99. data/lib/datadog/tracing/contrib/sneakers/integration.rb +15 -4
  100. data/lib/datadog/tracing/contrib/trilogy/configuration/settings.rb +6 -0
  101. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +3 -1
  102. data/lib/datadog/tracing/sampling/rate_sampler.rb +8 -19
  103. data/lib/datadog/tracing/transport/io/client.rb +5 -8
  104. data/lib/datadog/tracing/transport/io/traces.rb +28 -34
  105. data/lib/datadog/tracing/transport/traces.rb +4 -10
  106. data/lib/datadog/version.rb +1 -1
  107. metadata +17 -7
  108. data/lib/datadog/appsec/contrib/rails/ext.rb +0 -13
@@ -4,22 +4,19 @@ module Datadog
4
4
  module AppSec
5
5
  module Utils
6
6
  module HTTP
7
- # Implementation of media type for content negotiation
7
+ # Implementation of media type for HTTP headers
8
8
  #
9
9
  # See:
10
10
  # - https://www.rfc-editor.org/rfc/rfc7231#section-5.3.1
11
11
  # - https://www.rfc-editor.org/rfc/rfc7231#section-5.3.2
12
12
  class MediaType
13
- class ParseError < ::StandardError
14
- end
15
-
16
13
  WILDCARD = '*'
17
14
 
18
15
  # See: https://www.rfc-editor.org/rfc/rfc7230#section-3.2.6
19
16
  TOKEN_RE = /[-#$%&'*+.^_`|~A-Za-z0-9]+/.freeze
20
17
 
21
18
  # See: https://www.rfc-editor.org/rfc/rfc7231#section-3.1.1.1
22
- PARAMETER_RE = %r{ # rubocop:disable Style/RegexpLiteral
19
+ PARAMETER_RE = %r{
23
20
  (?:
24
21
  (?<parameter_name>#{TOKEN_RE})
25
22
  =
@@ -46,39 +43,49 @@ module Datadog
46
43
 
47
44
  attr_reader :type, :subtype, :parameters
48
45
 
49
- def initialize(media_type)
50
- media_type_match = MEDIA_TYPE_RE.match(media_type)
46
+ def self.parse(media)
47
+ match = MEDIA_TYPE_RE.match(media)
48
+ return if match.nil?
51
49
 
52
- raise ParseError, media_type.inspect if media_type_match.nil?
50
+ type = match['type'] || WILDCARD
51
+ type.downcase!
53
52
 
54
- @type = (media_type_match['type'] || WILDCARD).downcase
55
- @subtype = (media_type_match['subtype'] || WILDCARD).downcase
56
- @parameters = {}
53
+ subtype = match['subtype'] || WILDCARD
54
+ subtype.downcase!
57
55
 
58
- parameters = media_type_match['parameters']
56
+ parameters = {}
57
+ params = match['parameters']
59
58
 
60
- return if parameters.nil?
59
+ unless params.nil? || params.empty?
60
+ params.scan(PARAMETER_RE) do |name, unquoted_value, quoted_value|
61
+ # NOTE: Order of unquoted_value and quoted_value does not matter,
62
+ # as they are mutually exclusive by the regex.
63
+ # @type var value: ::String?
64
+ value = unquoted_value || quoted_value
65
+ next if name.nil? || value.nil?
61
66
 
62
- parameters.split(';').map(&:strip).each do |parameter|
63
- parameter_match = PARAMETER_RE.match(parameter)
67
+ # See https://github.com/soutaro/steep/issues/2051
68
+ name.downcase! # steep:ignore NoMethod
69
+ value.downcase!
64
70
 
65
- next if parameter_match.nil?
66
-
67
- parameter_name = parameter_match['parameter_name']
68
- parameter_value = parameter_match['parameter_value']
71
+ # See https://github.com/soutaro/steep/issues/2051
72
+ parameters[name] = value # steep:ignore ArgumentTypeMismatch
73
+ end
74
+ end
69
75
 
70
- next if parameter_name.nil? || parameter_value.nil?
76
+ new(type: type, subtype: subtype, parameters: parameters)
77
+ end
71
78
 
72
- @parameters[parameter_name.downcase] = parameter_value.downcase
73
- end
79
+ def initialize(type:, subtype:, parameters: {})
80
+ @type = type
81
+ @subtype = subtype
82
+ @parameters = parameters
74
83
  end
75
84
 
76
85
  def to_s
77
- s = +"#{@type}/#{@subtype}"
78
-
79
- s << ';' << @parameters.map { |k, v| "#{k}=#{v}" }.join(';') if @parameters.count > 0
86
+ return "#{@type}/#{@subtype}" if @parameters.empty?
80
87
 
81
- s
88
+ "#{@type}/#{@subtype};#{@parameters.map { |k, v| "#{k}=#{v}" }.join(";")}"
82
89
  end
83
90
  end
84
91
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Datadog
6
+ module AppSec
7
+ module Utils
8
+ module HTTP
9
+ # Module for parsing URL encoded payloads
10
+ module URLEncoded
11
+ # Parses a URL encoded payload (query string or form data) into a hash
12
+ # of keys and values, merging duplicate keys.
13
+ #
14
+ # Example:
15
+ #
16
+ # URLEncoded.parse("foo=bar&foo=baz&qux=quux") # => {"foo" => ["bar", "baz"], "qux" => "quux"}
17
+ #
18
+ # NOTE: Use it in the absence of `Rack::Utils.parse_query`
19
+ #
20
+ # WARNING: This method doesn't limit params byte size.
21
+ # See: https://github.com/rack/rack/blob/603b799de38b5eb9b2ff1657c8036a20f4c4db7b/lib/rack/query_parser.rb#L231-L233
22
+ def self.parse(payload)
23
+ return {} if payload.nil? || payload.empty?
24
+
25
+ payload.split('&').each_with_object({}) do |pair, memo|
26
+ next if pair.empty?
27
+
28
+ # NOTE: Steep has issues with mutation methods
29
+ # See https://github.com/ruby/rbs/issues/2819
30
+ #
31
+ # @type var key: ::String
32
+ # @type var value: ::String
33
+ key, value = pair.split('=', 2).map! do |value| #: ::String
34
+ CGI.unescape(value)
35
+ end
36
+
37
+ if (stored = memo[key])
38
+ if stored.is_a?(Array)
39
+ stored.push(value)
40
+ else
41
+ memo[key] = [stored, value]
42
+ end
43
+ else
44
+ memo[key] = value
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -11,6 +11,8 @@ require_relative '../runtime/metrics'
11
11
  require_relative '../telemetry/component'
12
12
  require_relative '../workers/runtime_metrics'
13
13
  require_relative '../remote/component'
14
+ require_relative '../utils/at_fork_monkey_patch'
15
+ require_relative '../utils/only_once'
14
16
  require_relative '../../tracing/component'
15
17
  require_relative '../../profiling/component'
16
18
  require_relative '../../appsec/component'
@@ -28,6 +30,9 @@ module Datadog
28
30
  module Configuration
29
31
  # Global components for the trace library.
30
32
  class Components
33
+ # Class-level constant to ensure fork patch is applied only once
34
+ AT_FORK_ONLY_ONCE = Utils::OnlyOnce.new
35
+
31
36
  class << self
32
37
  def build_health_metrics(settings, logger, telemetry)
33
38
  settings = settings.health_metrics
@@ -38,7 +43,7 @@ module Datadog
38
43
  end
39
44
 
40
45
  def build_logger(settings)
41
- logger = settings.logger.instance || Core::Logger.new($stdout)
46
+ logger = settings.logger.instance || Core::Logger.new($stderr)
42
47
  logger.level = settings.diagnostics.debug ? ::Logger::DEBUG : settings.logger.level
43
48
 
44
49
  logger
@@ -80,14 +85,15 @@ module Datadog
80
85
  Datadog::Core::Crashtracking::Component.build(settings, agent_settings, logger: logger)
81
86
  end
82
87
 
83
- def build_data_streams(settings, agent_settings, logger)
88
+ def build_data_streams(settings, agent_settings, logger, agent_info)
84
89
  return unless settings.data_streams.enabled
85
90
 
86
91
  Datadog::DataStreams::Processor.new(
87
92
  interval: settings.data_streams.interval,
88
93
  logger: logger,
89
94
  settings: settings,
90
- agent_settings: agent_settings
95
+ agent_settings: agent_settings,
96
+ agent_info: agent_info
91
97
  )
92
98
  rescue => e
93
99
  logger.warn("Failed to initialize Data Streams Monitoring: #{e.class}: #{e}")
@@ -121,6 +127,17 @@ module Datadog
121
127
  StableConfig.log_result(@logger)
122
128
  Deprecations.log_deprecations_from_all_sources(@logger)
123
129
 
130
+ # Register fork handling once globally
131
+ self.class::AT_FORK_ONLY_ONCE.run do
132
+ Utils::AtForkMonkeyPatch.apply!
133
+
134
+ # Register callback that calls Components.after_fork
135
+ Utils::AtForkMonkeyPatch.at_fork(:child) do
136
+ # Access via global to avoid capturing 'self'
137
+ Datadog.send(:components, allow_initialization: false)&.after_fork
138
+ end
139
+ end
140
+
124
141
  # This agent_settings is intended for use within Core. If you require
125
142
  # agent_settings within a product outside of core you should extend
126
143
  # the Core resolver from within your product/component's namespace.
@@ -150,13 +167,21 @@ module Datadog
150
167
  @open_feature = OpenFeature::Component.build(settings, agent_settings, logger: @logger, telemetry: telemetry)
151
168
  @dynamic_instrumentation = Datadog::DI::Component.build(settings, agent_settings, @logger, telemetry: telemetry)
152
169
  @error_tracking = Datadog::ErrorTracking::Component.build(settings, @tracer, @logger)
153
- @data_streams = self.class.build_data_streams(settings, agent_settings, @logger)
170
+ @data_streams = self.class.build_data_streams(settings, agent_settings, @logger, @agent_info)
154
171
  @environment_logger_extra[:dynamic_instrumentation_enabled] = !!@dynamic_instrumentation
155
172
 
156
173
  # Configure non-privileged components.
157
174
  Datadog::Tracing::Contrib::Component.configure(settings)
158
175
  end
159
176
 
177
+ # Called when a fork is detected
178
+ def after_fork
179
+ telemetry.after_fork
180
+ remote&.after_fork
181
+ crashtracker&.update_on_fork
182
+ ProcessDiscovery.after_fork
183
+ end
184
+
160
185
  # Hot-swaps with a new sampler.
161
186
  # This operation acquires the Components lock to ensure
162
187
  # there is no concurrent modification of the sampler.
@@ -16,8 +16,10 @@ module Datadog
16
16
  "DD_AI_GUARD_MAX_MESSAGES_LENGTH",
17
17
  "DD_AI_GUARD_TIMEOUT",
18
18
  "DD_API_KEY",
19
+ "DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE",
19
20
  "DD_API_SECURITY_ENABLED",
20
21
  "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED",
22
+ "DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS",
21
23
  "DD_API_SECURITY_REQUEST_SAMPLE_RATE",
22
24
  "DD_API_SECURITY_SAMPLE_DELAY",
23
25
  "DD_APM_TRACING_ENABLED",
@@ -42,12 +44,14 @@ module Datadog
42
44
  "DD_APP_KEY",
43
45
  "DD_CRASHTRACKING_ENABLED",
44
46
  "DD_DATA_STREAMS_ENABLED",
47
+ "DD_DBM_INJECT_SQL_BASEHASH",
45
48
  "DD_DBM_PROPAGATION_MODE",
46
49
  "DD_DISABLE_DATADOG_RAILS",
47
50
  "DD_DYNAMIC_INSTRUMENTATION_ENABLED",
48
51
  "DD_DYNAMIC_INSTRUMENTATION_PROBE_FILE",
49
52
  "DD_DYNAMIC_INSTRUMENTATION_REDACTED_IDENTIFIERS",
50
53
  "DD_DYNAMIC_INSTRUMENTATION_REDACTED_TYPES",
54
+ "DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS",
51
55
  "DD_ENV",
52
56
  "DD_ERROR_TRACKING_HANDLED_ERRORS",
53
57
  "DD_ERROR_TRACKING_HANDLED_ERRORS_INCLUDE",
@@ -284,7 +284,7 @@ module Datadog
284
284
  # This enables logging during initialization, otherwise we'd run into deadlocks.
285
285
 
286
286
  @temp_logger ||= begin
287
- logger = configuration.logger.instance || Core::Logger.new($stdout)
287
+ logger = configuration.logger.instance || Core::Logger.new($stderr)
288
288
  logger.level = configuration.diagnostics.debug ? ::Logger::DEBUG : configuration.logger.level
289
289
  logger
290
290
  end
@@ -298,7 +298,7 @@ module Datadog
298
298
  debug_env_value = DATADOG_ENV[Ext::Diagnostics::ENV_DEBUG_ENABLED]&.strip&.downcase
299
299
  debug_value = debug_env_value == 'true' || debug_env_value == '1'
300
300
 
301
- logger = Core::Logger.new($stdout)
301
+ logger = Core::Logger.new($stderr)
302
302
  # We cannot access config and the default level is INFO, so we need to set the level manually
303
303
  logger.level = debug_value ? ::Logger::DEBUG : ::Logger::INFO
304
304
  logger
@@ -3,8 +3,6 @@
3
3
  require 'libdatadog'
4
4
 
5
5
  require_relative 'tag_builder'
6
- require_relative '../utils/only_once'
7
- require_relative '../utils/at_fork_monkey_patch'
8
6
 
9
7
  module Datadog
10
8
  module Core
@@ -18,10 +16,8 @@ module Datadog
18
16
  #
19
17
  # Methods prefixed with _native_ are implemented in `crashtracker.c`
20
18
  class Component
21
- ONLY_ONCE = Core::Utils::OnlyOnce.new
22
-
23
19
  def self.build(settings, agent_settings, logger:)
24
- tags = TagBuilder.call(settings)
20
+ tags = latest_tags(settings)
25
21
  agent_base_url = agent_settings.url
26
22
 
27
23
  ld_library_path = ::Libdatadog.ld_library_path
@@ -45,6 +41,32 @@ module Datadog
45
41
  ).tap(&:start)
46
42
  end
47
43
 
44
+ # Reports unhandled exceptions to the crash tracker if available and appropriate.
45
+ # This is called from the at_exit hook to report unhandled exceptions.
46
+ def self.report_unhandled_exception(exception)
47
+ return unless exception && !exception.is_a?(SystemExit) && !exception.is_a?(NoMemoryError)
48
+
49
+ begin
50
+ crashtracker = Datadog.send(:components, allow_initialization: false)&.crashtracker
51
+ return unless crashtracker
52
+
53
+ crashtracker.report_unhandled_exception(exception)
54
+ rescue => e
55
+ # Unhandled exception report triggering means that the application is already in a bad state
56
+ # We don't want to swallow non-StandardError exceptions here; we would rather just let the
57
+ # application crash
58
+ Datadog.logger.debug("Crashtracker failed to report unhandled exception: #{e.message}")
59
+ end
60
+ end
61
+
62
+ # Gets the latest tags from the current configuration.
63
+ #
64
+ # We always fetch fresh tags because:
65
+ # After forking, we need the latest tags, not the parent's tags, such as the pid or runtime-id
66
+ def self.latest_tags(settings)
67
+ TagBuilder.call(settings)
68
+ end
69
+
48
70
  def initialize(tags:, agent_base_url:, ld_library_path:, path_to_crashtracking_receiver_binary:, logger:)
49
71
  @tags = tags
50
72
  @agent_base_url = agent_base_url
@@ -54,24 +76,62 @@ module Datadog
54
76
  end
55
77
 
56
78
  def start
57
- Utils::AtForkMonkeyPatch.apply!
58
-
59
79
  start_or_update_on_fork(action: :start, tags: tags)
60
-
61
- ONLY_ONCE.run do
62
- Utils::AtForkMonkeyPatch.at_fork(:child) do
63
- # Must NOT reference `self` here, as only the first instance will
64
- # be captured by the ONLY_ONCE and we want to pick the latest active one
65
- # (which may have different tags or agent config)
66
- Datadog.send(:components, allow_initialization: false)&.crashtracker&.update_on_fork
67
- end
68
- end
69
80
  end
70
81
 
71
82
  def update_on_fork(settings: Datadog.configuration)
72
- # Here we pick up the latest settings, so that we pick up any tags that change after forking
73
- # such as the pid or runtime-id
74
- start_or_update_on_fork(action: :update_on_fork, tags: TagBuilder.call(settings))
83
+ start_or_update_on_fork(action: :update_on_fork, tags: self.class.latest_tags(settings))
84
+ end
85
+
86
+ def report_unhandled_exception(exception, settings: Datadog.configuration)
87
+ # Maximum number of stack frames to include in exception crash reports
88
+ # This is the same number used for signal-based crashtracking's runtime stack
89
+ max_exception_stack_frames = 512
90
+
91
+ current_tags = self.class.latest_tags(settings)
92
+ # extract all frame data upfront; c expects exactly 3 elements, proper types, no nils
93
+ # limit to max_exception_stack_frames frames
94
+ all_backtrace_locations = exception.backtrace_locations || []
95
+ was_truncated = all_backtrace_locations.length > max_exception_stack_frames
96
+
97
+ backtrace_slice = all_backtrace_locations[0...max_exception_stack_frames] || []
98
+ # @type var frames_data: Array[[String, String, Integer]]
99
+ frames_data = backtrace_slice.map do |loc|
100
+ file = loc.path
101
+ file = '<unknown>' if file.nil? || file.empty? || !file.is_a?(String)
102
+
103
+ function = loc.label
104
+ function = '<unknown>' if function.nil? || function.empty? || !function.is_a?(String)
105
+
106
+ line = loc.lineno
107
+ line = 0 if line.nil? || line < 0 || !line.is_a?(Integer)
108
+
109
+ [file, function, line]
110
+ end
111
+
112
+ # Add truncation indicator frame if we had to cut off frames
113
+ if was_truncated
114
+ truncated_count = all_backtrace_locations.length - max_exception_stack_frames
115
+ frames_data << ['<truncated>', "<truncated #{truncated_count} more frames>", 0]
116
+ end
117
+
118
+ exception_message = exception.message
119
+ message =
120
+ if exception_message && !exception_message.empty?
121
+ "Process was terminated due to an unhandled exception of type '#{exception.class}'. Message: \"#{exception_message}\""
122
+ else
123
+ "Process was terminated due to an unhandled exception of type '#{exception.class}'."
124
+ end
125
+
126
+ success = self.class._native_report_ruby_exception(
127
+ agent_base_url,
128
+ message,
129
+ frames_data,
130
+ current_tags.to_a,
131
+ Datadog::VERSION::STRING
132
+ )
133
+
134
+ logger.debug('Crashtracker failed to report unhandled exception to crash tracker') unless success
75
135
  end
76
136
 
77
137
  def stop
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../tag_builder'
4
4
  require_relative '../utils'
5
+ require_relative '../environment/process'
5
6
 
6
7
  module Datadog
7
8
  module Core
@@ -13,6 +14,11 @@ module Datadog
13
14
  'is_crash' => 'true',
14
15
  )
15
16
 
17
+ if settings.experimental_propagate_process_tags_enabled
18
+ process_tags = Environment::Process.serialized
19
+ hash['process_tags'] = process_tags unless process_tags.empty?
20
+ end
21
+
16
22
  Utils.encode_tags(hash)
17
23
  end
18
24
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../utils/fnv'
4
+ require_relative 'process'
5
+
3
6
  module Datadog
4
7
  module Core
5
8
  module Environment
@@ -59,19 +62,80 @@ module Datadog
59
62
  @client = Remote::Transport::HTTP.root(agent_settings: agent_settings, logger: logger)
60
63
  end
61
64
 
62
- # Fetches the information from the agent.
65
+ # Fetches the information from the Trace Agent.
63
66
  # @return [Datadog::Core::Remote::Transport::HTTP::Negotiation::Response] the response from the agent
64
67
  # @return [nil] if an error occurred while fetching the information
65
68
  def fetch
66
69
  res = @client.send_info
67
70
  return unless res.ok?
68
71
 
72
+ update_propagation_checksum(res)
73
+
69
74
  res
70
75
  end
71
76
 
72
77
  def ==(other)
73
78
  other.is_a?(self.class) && other.agent_settings == agent_settings
74
79
  end
80
+
81
+ # Returns the propagation checksum, comprising of process tags and optionally container tags (from the Trace Agent)
82
+ # Currently called/used by the DBM code to inject the propagation checksum into the SQL comment.
83
+ #
84
+ # This checksum is used for correlation across signals (traces, DBM, data streams, etc.) in environments
85
+ #
86
+ # The checksum is populated by the trace transport's periodic fetch calls.
87
+ # @return [Integer, nil] the FNV hash based on the container and process tags or nil
88
+ attr_reader :propagation_checksum
89
+
90
+ private
91
+
92
+ # Trace Agent 7.69.0+ provides a SHA256 checksum in the response header DATADOG-CONTAINER-TAGS-HASH based on the container id computed in Datadog::Core::Environment::Container
93
+ # This is a short checksum that uniquely identifies this process, its container environment, and
94
+ # the Datadog agent it connects to.
95
+ #
96
+ # This checksum only has to be internally consistent: the same value must be used by every signal
97
+ # emitted by this process+container+agent combinations). It is not required that this checksum is
98
+ # consistent with other SDKs.
99
+ # During calls to the Trace Agent, this checksum is cached but invalidated if a new value is returned
100
+ # The resulting propagation_checksum uses the container_tags_checksum
101
+ # https://github.com/DataDog/datadog-agent/pull/38515
102
+ attr_reader :container_tags_checksum
103
+
104
+ # Setting DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED to true enables the following behavior:
105
+ # - Computes a propagation checksum from process tags and optionally container tags when tags change
106
+ # - This is needed in traces (dsm and dbm related spans), DBM, and DSM.
107
+ #
108
+ # Container tags extraction:
109
+ # Datadog::Core::Environment::Container extracts the container id from the cgroup folder if possible
110
+ # (note: not currently available in cgroupv2) and sends it to the Trace Agent via the header Datadog-Container-ID.
111
+ # The Trace Agent takes the container id and looks for matching container tags to compute a SHA256 checksum via the response header DATADOG-CONTAINER-TAGS-HASH
112
+ # https://github.com/DataDog/datadog-agent/blob/c923da011c8e51c35c0d05b6b10d016521915e7d/pkg/trace/api/info.go#L203-L227
113
+ #
114
+ # When deciding whether the propagation checksum should be updated, we need to be aware of some concerns:
115
+ # - The tracer fails to send the container id in the first place
116
+ # - It is possible that older Trace Agents may not have this specific header
117
+ # - It is possible that we don't have access to the value if the Trace Agent is temporarily down. In these cases, we need to check for the value again on the next call to the info endpoint
118
+ # - If we have access to the value, we need to check if it changed from the previous value.
119
+ # - The Trace Agent runs into a permissions/setup issue.
120
+ def update_propagation_checksum(res)
121
+ return unless Datadog.configuration.experimental_propagate_process_tags_enabled
122
+
123
+ header_value = res.headers[Core::Transport::Ext::HTTP::HEADER_CONTAINER_TAGS_HASH]
124
+ new_container_tags_checksum = header_value if header_value && !header_value.empty?
125
+
126
+ # if the Trace Agent returns a new value for the checksum, calculate and cache the propagation checksum
127
+ # If there was no previous propagation_checksum, then we should calculate the checksum by checking the agent and getting process info
128
+ if @propagation_checksum.nil? || (new_container_tags_checksum && new_container_tags_checksum != @container_tags_checksum)
129
+ @container_tags_checksum = new_container_tags_checksum
130
+
131
+ checksum_input = Process.serialized
132
+ # Use local variable for Steep type narrowing (helps Steep with type narrowing)
133
+ container_checksum = @container_tags_checksum
134
+ checksum_input += container_checksum if container_checksum
135
+
136
+ @propagation_checksum = Core::Utils::FNV.fnv1_64(checksum_input)
137
+ end
138
+ end
75
139
  end
76
140
  end
77
141
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Core
5
+ # Deterministic sampler using Knuth multiplicative hash algorithm.
6
+ #
7
+ # This sampler provides consistent sampling decisions based on an input value,
8
+ # ensuring the same input always produces the same sampling decision for a given rate.
9
+ #
10
+ # The algorithm multiplies the input by a large prime (Knuth factor), takes modulo
11
+ # to constrain to a fixed range, and compares against a threshold derived from the sample rate.
12
+ #
13
+ # @api private
14
+ # @see https://en.wikipedia.org/wiki/Hash_function#Multiplicative_hashing
15
+ class KnuthSampler
16
+ # Maximum unsigned 64-bit integer for uniform distribution across 64-bit input space.
17
+ UINT64_MAX = (1 << 64) - 1
18
+ UINT64_MODULO = 1 << 64
19
+
20
+ # Golden ratio constant for optimal distribution.
21
+ # @see https://en.wikipedia.org/wiki/Hash_function#Fibonacci_hashing
22
+ DEFAULT_KNUTH_FACTOR = 11400714819323198485
23
+
24
+ attr_reader :rate
25
+
26
+ # @param rate [Float] Sampling rate between +0.0+ and +1.0+ (inclusive).
27
+ # +0.0+ means no samples are kept; +1.0+ means all samples are kept.
28
+ # Invalid values fall back to +1.0+ (sample everything).
29
+ # @param knuth_factor [Integer] Multiplicative constant for hashing.
30
+ # Different factors produce different sampling distributions.
31
+ def initialize(rate = 1.0, knuth_factor: DEFAULT_KNUTH_FACTOR)
32
+ @knuth_factor = knuth_factor
33
+
34
+ rate = rate.to_f
35
+ unless rate >= 0.0 && rate <= 1.0
36
+ Datadog.logger.warn("Sample rate #{rate} is not between 0.0 and 1.0, falling back to 1.0")
37
+ rate = 1.0
38
+ end
39
+
40
+ @rate = rate
41
+ @threshold = @rate * UINT64_MAX
42
+ end
43
+
44
+ # Determines if the given input should be sampled.
45
+ #
46
+ # This method is deterministic: the same input value always produces
47
+ # the same result for a given sample rate and configuration.
48
+ #
49
+ # @param input [Integer] Value to determine sampling decision.
50
+ # Typically a trace ID or incrementing counter.
51
+ # @return [Boolean] +true+ if input should be sampled, +false+ otherwise
52
+ def sample?(input)
53
+ ((input * @knuth_factor) % UINT64_MODULO) <= @threshold
54
+ end
55
+ end
56
+ end
57
+ end
@@ -28,7 +28,7 @@ module Datadog
28
28
 
29
29
  if message.nil?
30
30
  if block
31
- super do
31
+ super(severity, nil, progname) do
32
32
  "[#{self.progname}] #{where}#{yield}"
33
33
  end
34
34
  else
@@ -12,7 +12,7 @@ module Datadog
12
12
  attr_accessor :logger
13
13
 
14
14
  def initialize(logger = nil)
15
- @logger = logger || Logger.new($stdout).tap do |l|
15
+ @logger = logger || Logger.new($stderr).tap do |l|
16
16
  l.level = ::Logger::INFO
17
17
  l.progname = nil
18
18
  l.formatter = proc do |_severity, datetime, _progname, msg|
@@ -1,16 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'datadog/core/process_discovery/tracer_memfd'
4
-
5
- require_relative 'utils/at_fork_monkey_patch'
6
- require_relative 'utils/only_once'
3
+ require_relative 'process_discovery/tracer_memfd'
4
+ require_relative 'environment/process'
5
+ require_relative 'environment/container'
7
6
 
8
7
  module Datadog
9
8
  module Core
10
9
  # Class used to store tracer metadata in a native file descriptor.
11
10
  module ProcessDiscovery
12
- ONLY_ONCE = Core::Utils::OnlyOnce.new
13
-
14
11
  class << self
15
12
  def publish(settings)
16
13
  if (libdatadog_api_failure = Datadog::Core::LIBDATADOG_API_FAILURE)
@@ -18,8 +15,6 @@ module Datadog
18
15
  return
19
16
  end
20
17
 
21
- ONLY_ONCE.run { apply_at_fork_patch }
22
-
23
18
  metadata = get_metadata(settings)
24
19
 
25
20
  shutdown!
@@ -27,8 +22,15 @@ module Datadog
27
22
  end
28
23
 
29
24
  def shutdown!
30
- @file_descriptor&.shutdown!(Datadog.logger)
31
- @file_descriptor = nil
25
+ if defined?(@file_descriptor)
26
+ @file_descriptor&.shutdown!(Datadog.logger)
27
+ @file_descriptor = nil
28
+ end
29
+ end
30
+
31
+ def after_fork
32
+ # The runtime-id changes after a fork. We call publish to ensure that the runtime-id is updated.
33
+ publish(Datadog.configuration)
32
34
  end
33
35
 
34
36
  private
@@ -44,17 +46,11 @@ module Datadog
44
46
  service_name: settings.service || '',
45
47
  service_env: settings.env || '',
46
48
  service_version: settings.version || '',
47
- # TODO: Implement process tags and container id
48
- process_tags: '',
49
- container_id: ''
49
+ # Follows Java: https://github.com/DataDog/dd-trace-java/blob/2ebc964340ac530342cc389ba68ff0f5070d5f9f/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java#L37-L38
50
+ process_tags: settings.experimental_propagate_process_tags_enabled ? Core::Environment::Process.serialized : '',
51
+ container_id: Core::Environment::Container.container_id || ''
50
52
  }
51
53
  end
52
-
53
- def apply_at_fork_patch
54
- # The runtime-id changes after a fork. We apply this patch to at_fork to ensure that the runtime-id is updated.
55
- Utils::AtForkMonkeyPatch.apply!
56
- Utils::AtForkMonkeyPatch.at_fork(:child) { publish(Datadog.configuration) }
57
- end
58
54
  end
59
55
  end
60
56
  end