datadog 2.10.0 → 2.11.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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +3 -3
  4. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +44 -1
  5. data/ext/datadog_profiling_native_extension/extconf.rb +4 -0
  6. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.c +2 -0
  7. data/ext/datadog_profiling_native_extension/gvl_profiling_helper.h +0 -8
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
  9. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +56 -0
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +7 -0
  11. data/ext/datadog_profiling_native_extension/profiling.c +7 -0
  12. data/ext/libdatadog_api/crashtracker.c +4 -4
  13. data/ext/libdatadog_extconf_helpers.rb +1 -1
  14. data/lib/datadog/appsec/configuration/settings.rb +64 -11
  15. data/lib/datadog/appsec/contrib/active_record/patcher.rb +0 -3
  16. data/lib/datadog/appsec/contrib/devise/configuration.rb +76 -0
  17. data/lib/datadog/appsec/contrib/devise/event.rb +4 -7
  18. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +16 -21
  19. data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +8 -15
  20. data/lib/datadog/appsec/contrib/devise/patcher/rememberable_patch.rb +1 -1
  21. data/lib/datadog/appsec/contrib/devise/patcher.rb +0 -3
  22. data/lib/datadog/appsec/contrib/devise/tracking.rb +1 -1
  23. data/lib/datadog/appsec/contrib/excon/integration.rb +41 -0
  24. data/lib/datadog/appsec/contrib/excon/patcher.rb +28 -0
  25. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +43 -0
  26. data/lib/datadog/appsec/contrib/faraday/connection_patch.rb +22 -0
  27. data/lib/datadog/appsec/contrib/faraday/integration.rb +42 -0
  28. data/lib/datadog/appsec/contrib/faraday/patcher.rb +53 -0
  29. data/lib/datadog/appsec/contrib/faraday/rack_builder_patch.rb +22 -0
  30. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +42 -0
  31. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +10 -12
  32. data/lib/datadog/appsec/contrib/graphql/patcher.rb +0 -3
  33. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +65 -73
  34. data/lib/datadog/appsec/contrib/rack/patcher.rb +0 -3
  35. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +20 -25
  36. data/lib/datadog/appsec/contrib/rails/patcher.rb +0 -3
  37. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +38 -49
  38. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +0 -3
  39. data/lib/datadog/appsec/monitor/gateway/watcher.rb +19 -25
  40. data/lib/datadog/appsec/remote.rb +4 -0
  41. data/lib/datadog/appsec.rb +2 -0
  42. data/lib/datadog/core/configuration/components.rb +7 -1
  43. data/lib/datadog/core/configuration/ext.rb +1 -1
  44. data/lib/datadog/core/configuration/option_definition.rb +2 -0
  45. data/lib/datadog/core/configuration/settings.rb +22 -6
  46. data/lib/datadog/core/encoding.rb +16 -0
  47. data/lib/datadog/core/environment/agent_info.rb +77 -0
  48. data/lib/datadog/core/remote/transport/http/api.rb +13 -18
  49. data/lib/datadog/core/remote/transport/http/config.rb +0 -18
  50. data/lib/datadog/core/remote/transport/http/negotiation.rb +1 -18
  51. data/lib/datadog/core/remote/transport/http.rb +7 -12
  52. data/lib/datadog/core/remote/transport/negotiation.rb +13 -1
  53. data/lib/datadog/core/telemetry/event.rb +5 -0
  54. data/lib/datadog/core/transport/http/adapters/unix_socket.rb +1 -1
  55. data/lib/datadog/{tracing → core}/transport/http/api/instance.rb +1 -1
  56. data/lib/datadog/{tracing → core}/transport/http/api/spec.rb +1 -1
  57. data/lib/datadog/{tracing → core}/transport/http/builder.rb +37 -17
  58. data/lib/datadog/core/transport/response.rb +4 -0
  59. data/lib/datadog/di/code_tracker.rb +15 -8
  60. data/lib/datadog/di/component.rb +1 -0
  61. data/lib/datadog/di/configuration/settings.rb +14 -0
  62. data/lib/datadog/di/contrib.rb +2 -0
  63. data/lib/datadog/di/logger.rb +30 -0
  64. data/lib/datadog/di/probe.rb +3 -6
  65. data/lib/datadog/di/probe_manager.rb +5 -2
  66. data/lib/datadog/di/probe_notifier_worker.rb +15 -4
  67. data/lib/datadog/di/remote.rb +3 -3
  68. data/lib/datadog/di/utils.rb +91 -0
  69. data/lib/datadog/di.rb +3 -0
  70. data/lib/datadog/profiling/component.rb +2 -8
  71. data/lib/datadog/profiling/load_native_extension.rb +1 -33
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/contrib/extensions.rb +14 -0
  74. data/lib/datadog/tracing/contrib/graphql/configuration/error_extension_env_parser.rb +21 -0
  75. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +11 -0
  76. data/lib/datadog/tracing/contrib/graphql/ext.rb +5 -0
  77. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +102 -11
  78. data/lib/datadog/tracing/contrib/rack/header_collection.rb +11 -1
  79. data/lib/datadog/tracing/contrib/rack/middlewares.rb +1 -1
  80. data/lib/datadog/tracing/contrib/span_attribute_schema.rb +6 -1
  81. data/lib/datadog/tracing/transport/http/api.rb +11 -2
  82. data/lib/datadog/tracing/transport/http/traces.rb +0 -3
  83. data/lib/datadog/tracing/transport/http.rb +12 -7
  84. data/lib/datadog/tracing/transport/serializable_trace.rb +8 -4
  85. data/lib/datadog/tracing/transport/traces.rb +25 -8
  86. data/lib/datadog/version.rb +1 -1
  87. metadata +23 -28
  88. data/ext/datadog_profiling_loader/datadog_profiling_loader.c +0 -142
  89. data/ext/datadog_profiling_loader/extconf.rb +0 -60
  90. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +0 -46
  91. data/lib/datadog/appsec/contrib/patcher.rb +0 -12
  92. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +0 -69
  93. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +0 -47
  94. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +0 -53
  95. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +0 -53
  96. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +0 -48
  97. data/lib/datadog/appsec/monitor/reactive/set_user.rb +0 -45
  98. data/lib/datadog/appsec/reactive/address_hash.rb +0 -22
  99. data/lib/datadog/appsec/reactive/engine.rb +0 -47
  100. data/lib/datadog/appsec/reactive/subscriber.rb +0 -19
  101. data/lib/datadog/core/remote/transport/http/api/instance.rb +0 -39
  102. data/lib/datadog/core/remote/transport/http/api/spec.rb +0 -21
  103. data/lib/datadog/core/remote/transport/http/builder.rb +0 -219
@@ -10,7 +10,7 @@ module Datadog
10
10
  # The loop inside the worker rescues all exceptions to prevent termination
11
11
  # due to unhandled exceptions raised by any downstream code.
12
12
  # This includes communication and protocol errors when sending the
13
- # payloads to the agent.
13
+ # events to the agent.
14
14
  #
15
15
  # The worker groups the data to send into batches. The goal is to perform
16
16
  # no more than one network operation per event type per second.
@@ -36,6 +36,7 @@ module Datadog
36
36
  @sleep_remaining = nil
37
37
  @wake_scheduled = false
38
38
  @thread = nil
39
+ @pid = nil
39
40
  @flush = 0
40
41
  end
41
42
 
@@ -44,7 +45,8 @@ module Datadog
44
45
  attr_reader :telemetry
45
46
 
46
47
  def start
47
- return if @thread
48
+ return if @thread && @pid == Process.pid
49
+ logger.trace { "di: starting probe notifier: pid #{$$}" }
48
50
  @thread = Thread.new do
49
51
  loop do
50
52
  # TODO If stop is requested, we stop immediately without
@@ -86,6 +88,7 @@ module Datadog
86
88
  wake.wait(more ? min_send_interval : nil)
87
89
  end
88
90
  end
91
+ @pid = Process.pid
89
92
  end
90
93
 
91
94
  # Stops the background thread.
@@ -94,6 +97,7 @@ module Datadog
94
97
  # to killing the thread using Thread#kill.
95
98
  def stop(timeout = 1)
96
99
  @stop_requested = true
100
+ logger.trace { "di: stopping probe notifier: pid #{$$}" }
97
101
  wake.signal
98
102
  if thread
99
103
  unless thread.join(timeout)
@@ -184,8 +188,9 @@ module Datadog
184
188
  @lock.synchronize do
185
189
  queue = send("#{event_type}_queue")
186
190
  if queue.length > settings.dynamic_instrumentation.internal.snapshot_queue_capacity
187
- logger.debug { "di: #{self.class.name}: dropping #{event_type} because queue is full" }
191
+ logger.debug { "di: #{self.class.name}: dropping #{event_type} event because queue is full" }
188
192
  else
193
+ logger.trace { "di: #{self.class.name}: queueing #{event_type} event" }
189
194
  queue << event
190
195
  end
191
196
  end
@@ -200,6 +205,10 @@ module Datadog
200
205
  wake.signal
201
206
  end
202
207
  end
208
+
209
+ # Worker could be not running if the process forked - check and
210
+ # start it again in this case.
211
+ start
203
212
  end
204
213
 
205
214
  # Determine how much longer the worker thread should sleep
@@ -232,8 +241,10 @@ module Datadog
232
241
  instance_variable_set("@#{event_type}_queue", [])
233
242
  @io_in_progress = batch.any? # steep:ignore
234
243
  end
244
+ logger.trace { "di: #{self.class.name}: checking #{event_type} queue - #{batch.length} entries" } # steep:ignore
235
245
  if batch.any? # steep:ignore
236
246
  begin
247
+ logger.trace { "di: sending #{batch.length} #{event_type} event(s) to agent" } # steep:ignore
237
248
  transport.public_send("send_#{event_type}", batch)
238
249
  time = Core::Utils::Time.get_time
239
250
  @lock.synchronize do
@@ -263,7 +274,7 @@ module Datadog
263
274
 
264
275
  def maybe_send
265
276
  rv = maybe_send_status
266
- rv || maybe_send_snapshot
277
+ maybe_send_snapshot || rv
267
278
  end
268
279
  end
269
280
  end
@@ -53,7 +53,7 @@ module Datadog
53
53
  payload = probe_notification_builder.build_received(probe)
54
54
  probe_notifier_worker = component.probe_notifier_worker
55
55
  probe_notifier_worker.add_status(payload)
56
- component.logger.debug { "di: received probe from RC: #{probe.type} #{probe.location}" }
56
+ component.logger.debug { "di: received #{probe.type} probe at #{probe.location} (#{probe.id}) via RC" }
57
57
 
58
58
  begin
59
59
  # TODO test exception capture
@@ -76,7 +76,7 @@ module Datadog
76
76
  rescue => exc
77
77
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
78
78
 
79
- component.logger.debug { "di: unhandled exception adding probe in DI remote receiver: #{exc.class}: #{exc}" }
79
+ component.logger.debug { "di: unhandled exception adding #{probe.type} probe at #{probe.location} (#{probe.id}) in DI remote receiver: #{exc.class}: #{exc}" }
80
80
  component.telemetry&.report(exc, description: "Unhandled exception adding probe in DI remote receiver")
81
81
 
82
82
  # TODO test this path
@@ -101,7 +101,7 @@ module Datadog
101
101
  rescue => exc
102
102
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
103
103
 
104
- component.logger.debug { "di: unhandled exception handling probe in DI remote receiver: #{exc.class}: #{exc}" }
104
+ component.logger.debug { "di: unhandled exception handling a probe in DI remote receiver: #{exc.class}: #{exc}" }
105
105
  component.telemetry&.report(exc, description: "Unhandled exception handling probe in DI remote receiver")
106
106
 
107
107
  # TODO assert content state (errored for this example)
@@ -3,6 +3,82 @@
3
3
  module Datadog
4
4
  module DI
5
5
  module Utils
6
+ # General path matching considerations
7
+ # ------------------------------------
8
+ #
9
+ # The following use cases must be supported:
10
+ # 1. The "probe path" is relative path to the file from source code
11
+ # repository root. The project is deployed from the repository root,
12
+ # such that that same relative path exists at runtime from the
13
+ # root of the application.
14
+ # 2. The "probe path" is a relative path to the file in a monorepo
15
+ # where the project being deployed is in a subdirectory.
16
+ # This the "probe path" contains additional directory components
17
+ # in the beginning that do not exist in the runtime environment.
18
+ # 3. The "probe path" is an absolute path to the file on the customer's
19
+ # development system. As specified this path definitely does not
20
+ # exist at runtime, and can start with a prefix that is unknown
21
+ # to any both UI and tracer code.
22
+ # 4. Same as (3), but the customer is using a Windows computer for
23
+ # development and has the path specified in the wrong case
24
+ # (which works fine on their development machine).
25
+ # 5. The "probe path" is the basename or any suffix of the path to
26
+ # the desired file, typed manually by the customer into the UI.
27
+ #
28
+ # A related concern is that if multiple runtime paths match the path
29
+ # specification in the probe, the tracer must return an error to the
30
+ # backend/UI rather than instrumenting any of the matching paths.
31
+ #
32
+ # The logic for path matching should therefore, generally, be as follows:
33
+ # 1. If the "probe path" is absolute, see if it exists at runtime.
34
+ # If so, take it as the desired path and finish.
35
+ # 2. Attempt to identify the application root, by checking if the current
36
+ # working directory contains a file called Gemfile. If yes, assume
37
+ # the current working directory is the application root, otherwise
38
+ # consider the application root to be unknown.
39
+ # 3. If the application root is known and the "probe path" is relative,
40
+ # concatenate the "probe path" to the application root and check
41
+ # if the resulting path exists at runtime. If so, take it as the
42
+ # desired path and finish.
43
+ # 4. If the "probe path" is relative, go through the known file paths,
44
+ # filter these paths down to those whose suffix is the "probe path",
45
+ # and check how many we are left with. If exactly one, assume this
46
+ # is the desired path and finish. If more than one, return an error
47
+ # "multiple matching paths".
48
+ # 5. If the application root is known, for each suffix of the "probe path",
49
+ # see if that relative paths concatenated to the application root
50
+ # results in a known file. If a known file is found, assume this
51
+ # is the wanted file and finish.
52
+ # 6. For each suffix of the "probe path", filter the set of known paths
53
+ # down to those that end in the suffix. If exactly one path remains
54
+ # for a given suffix, assume this is the wanted path and finish.
55
+ # If more than one path remains for a given suffix, return the error
56
+ # "multiple matching paths".
57
+ # 7. Repeat step 5 but perform case-insensitive comparison.
58
+ # 8. Repeat step 6 but perform case-insensitive comparison.
59
+ #
60
+ # Note that we do not look for "probe paths" under the current working
61
+ # directory at runtime if the current working directory does not contain
62
+ # a Gemfile, to avoid finding files from potentially undesired bases.
63
+ #
64
+ # What "known file"/"known path" means also differs based on the
65
+ # operation being performed:
66
+ # - If the operation is "install a probe", "known file/path" can
67
+ # include files on the filesystem that have not been loaded as
68
+ # well as paths from the code tracker.
69
+ # - If the operation is "check if any pending probes match the file
70
+ # that was just loaded", we would only consider the path that was
71
+ # just loaded and not check the filesystem.
72
+ #
73
+ # Filesystem inquiries are obviously quite expensive and should be
74
+ # cached. For the vast majority of applications it should be safe to
75
+ # indefinitely cache whether a particular filesystem paths exists
76
+ # in both positive and negative.
77
+ #
78
+ # As a "quick fix", currently after performing the suffix matching
79
+ # we just strip leading directory components from the "probe path"
80
+ # until we get a match via a "suffix of the suffix".
81
+
6
82
  # Returns whether the provided +path+ matches the user-designated
7
83
  # file suffix (of a line probe).
8
84
  #
@@ -41,6 +117,21 @@ module Datadog
41
117
  # !!(path =~ %r,(/|\A)#{Regexp.quote(suffix)}\z,)
42
118
  end
43
119
  end
120
+
121
+ # Returns whether the provided +path+ matches the "probe path" in
122
+ # +spec+. Attempts all of the fuzzy matches by stripping directories
123
+ # from the front of +spec+. Does not consider othr known paths to
124
+ # identify the case of (potentially) multiple matching paths for +spec+.
125
+ module_function def path_can_match_spec?(path, spec)
126
+ return true if path_matches_suffix?(path, spec)
127
+
128
+ spec = spec.dup
129
+ loop do
130
+ return false unless spec.include?('/')
131
+ spec.sub!(%r{.*/+}, '')
132
+ return true if path_matches_suffix?(path, spec)
133
+ end
134
+ end
44
135
  end
45
136
  end
46
137
  end
data/lib/datadog/di.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'di/logger'
3
4
  require_relative 'di/base'
4
5
  require_relative 'di/error'
5
6
  require_relative 'di/code_tracker'
@@ -58,6 +59,8 @@ if %w(1 true).include?(ENV['DD_DYNAMIC_INSTRUMENTATION_ENABLED']) # steep:ignore
58
59
  #
59
60
  # If DI is enabled programmatically, the application can (and must,
60
61
  # for line probes to work) activate tracking in an initializer.
62
+ # We seem to have Datadog.logger here already
63
+ Datadog.logger.debug("di: activating code tracking")
61
64
  Datadog::DI.activate_tracking
62
65
  end
63
66
 
@@ -435,17 +435,11 @@ module Datadog
435
435
  end
436
436
 
437
437
  private_class_method def self.enable_gvl_profiling?(settings, logger)
438
- if RUBY_VERSION < "3.2"
439
- if settings.profiling.advanced.preview_gvl_enabled
440
- logger.warn("GVL profiling is currently not supported in Ruby < 3.2 and will not be enabled.")
441
- end
442
-
443
- return false
444
- end
438
+ return false if RUBY_VERSION < "3.2"
445
439
 
446
440
  # GVL profiling only makes sense in the context of timeline. We could emit a warning here, but not sure how
447
441
  # useful it is -- if a customer disables timeline, there's nowhere to look for GVL profiling anyway!
448
- settings.profiling.advanced.timeline_enabled && settings.profiling.advanced.preview_gvl_enabled
442
+ settings.profiling.advanced.timeline_enabled && settings.profiling.advanced.gvl_enabled
449
443
  end
450
444
  end
451
445
  end
@@ -1,41 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This file is used to load the profiling native extension. It works in two steps:
4
- #
5
- # 1. Load the datadog_profiling_loader extension. This extension will be used to load the actual extension, but in
6
- # a special way that avoids exposing native-level code symbols. See `datadog_profiling_loader.c` for more details.
7
- #
8
- # 2. Use the Datadog::Profiling::Loader exposed by the datadog_profiling_loader extension to load the actual
9
- # profiling native extension.
10
- #
11
- # All code on this file is on-purpose at the top-level; this makes it so this file is executed only once,
12
- # the first time it gets required, to avoid any issues with the native extension being initialized more than once.
13
-
14
3
  begin
15
- require "datadog_profiling_loader.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
4
+ require "datadog_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
16
5
  rescue LoadError => e
17
6
  raise LoadError,
18
7
  "Failed to load the profiling loader extension. To fix this, please remove and then reinstall datadog " \
19
8
  "(Details: #{e.message})"
20
9
  end
21
-
22
- extension_name = "datadog_profiling_native_extension.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
23
- file_name = "#{extension_name}.#{RbConfig::CONFIG["DLEXT"]}"
24
- full_file_path = "#{__dir__}/../../#{file_name}"
25
-
26
- unless File.exist?(full_file_path)
27
- extension_dir = Gem.loaded_specs["datadog"].extension_dir
28
- candidate_path = "#{extension_dir}/#{file_name}"
29
- if File.exist?(candidate_path)
30
- full_file_path = candidate_path
31
- else
32
- # We found none of the files. This is unexpected. Let's go ahead anyway, the error is going to be reported further
33
- # down anyway.
34
- end
35
- end
36
-
37
- init_function_name = "Init_#{extension_name.split(".").first}"
38
-
39
- status, result = Datadog::Profiling::Loader._native_load(full_file_path, init_function_name)
40
-
41
- raise "Failure to load #{extension_name} due to #{result}" if status == :error
@@ -16,6 +16,7 @@ module Datadog
16
16
  # @public_api
17
17
  module SpanAttributeSchema
18
18
  ENV_GLOBAL_DEFAULT_SERVICE_NAME_ENABLED = 'DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED'
19
+ ENV_PEER_SERVICE_DEFAULTS_ENABLED = 'DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED'
19
20
  ENV_PEER_SERVICE_MAPPING = 'DD_TRACE_PEER_SERVICE_MAPPING'
20
21
  end
21
22
 
@@ -130,6 +130,20 @@ module Datadog
130
130
  o.default({})
131
131
  end
132
132
 
133
+ # Enables population of default in the `peer.service` span tag.
134
+ # Explicitly setting the `peer.service` for an integration will
135
+ # still be honored with this option disabled.
136
+ #
137
+ # Also when disabled, other peer service related configurations have no effect.
138
+ #
139
+ # @default `DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED` environment variable, otherwise `false`
140
+ # @return [Boolean]
141
+ option :peer_service_defaults do |o|
142
+ o.env Tracing::Configuration::Ext::SpanAttributeSchema::ENV_PEER_SERVICE_DEFAULTS_ENABLED
143
+ o.type :bool
144
+ o.default false
145
+ end
146
+
133
147
  # Global service name behavior
134
148
  settings :global_default_service_name do
135
149
  # Overrides default service name to global service name
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Tracing
5
+ module Contrib
6
+ module GraphQL
7
+ module Configuration
8
+ # Parses the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` for error extension names declaration.
9
+ class ErrorExtensionEnvParser
10
+ # Parses the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` into an array of error extension names.
11
+ def self.call(values)
12
+ # Split by comma, remove leading and trailing whitespaces,
13
+ # remove empty values, and remove repeated values.
14
+ values.split(',').each(&:strip!).reject(&:empty?).uniq
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../../configuration/settings'
4
4
  require_relative '../ext'
5
+ require_relative 'error_extension_env_parser'
5
6
 
6
7
  module Datadog
7
8
  module Tracing
@@ -44,9 +45,19 @@ module Datadog
44
45
  end
45
46
 
46
47
  option :with_unified_tracer do |o|
48
+ o.env Ext::ENV_WITH_UNIFIED_TRACER
47
49
  o.type :bool
48
50
  o.default false
49
51
  end
52
+
53
+ # Capture error extensions provided by the user in their GraphQL error responses.
54
+ # The extensions can be anything, so the user is responsible for ensuring they are safe to capture.
55
+ option :error_extensions do |o|
56
+ o.env Ext::ENV_ERROR_EXTENSIONS
57
+ o.type :array, nilable: false
58
+ o.default []
59
+ o.env_parser { |v| ErrorExtensionEnvParser.call(v) }
60
+ end
50
61
  end
51
62
  end
52
63
  end
@@ -11,8 +11,13 @@ module Datadog
11
11
  # @!visibility private
12
12
  ENV_ANALYTICS_ENABLED = 'DD_TRACE_GRAPHQL_ANALYTICS_ENABLED'
13
13
  ENV_ANALYTICS_SAMPLE_RATE = 'DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE'
14
+ ENV_WITH_UNIFIED_TRACER = 'DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER'
15
+ ENV_ERROR_EXTENSIONS = 'DD_TRACE_GRAPHQL_ERROR_EXTENSIONS'
14
16
  SERVICE_NAME = 'graphql'
15
17
  TAG_COMPONENT = 'graphql'
18
+
19
+ # Span event name for query-level errors
20
+ EVENT_QUERY_ERROR = 'dd.graphql.query.error'
16
21
  end
17
22
  end
18
23
  end
@@ -47,14 +47,26 @@ module Datadog
47
47
  end
48
48
 
49
49
  def execute_query(*args, query:, **kwargs)
50
- trace(proc { super }, 'execute', query.selected_operation_name, query: query) do |span|
51
- span.set_tag('graphql.source', query.query_string)
52
- span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
53
- span.set_tag('graphql.operation.name', query.selected_operation_name) if query.selected_operation_name
54
- query.variables.instance_variable_get(:@storage).each do |key, value|
55
- span.set_tag("graphql.variables.#{key}", value)
56
- end
57
- end
50
+ trace(
51
+ proc { super },
52
+ 'execute',
53
+ query.selected_operation_name,
54
+ lambda { |span|
55
+ span.set_tag('graphql.source', query.query_string)
56
+ span.set_tag('graphql.operation.type', query.selected_operation.operation_type)
57
+ if query.selected_operation_name
58
+ span.set_tag(
59
+ 'graphql.operation.name',
60
+ query.selected_operation_name
61
+ )
62
+ end
63
+ query.variables.instance_variable_get(:@storage).each do |key, value|
64
+ span.set_tag("graphql.variables.#{key}", value)
65
+ end
66
+ },
67
+ ->(span) { add_query_error_events(span, query.context.errors) },
68
+ query: query,
69
+ )
58
70
  end
59
71
 
60
72
  def execute_query_lazy(*args, query:, multiplex:, **kwargs)
@@ -131,7 +143,16 @@ module Datadog
131
143
 
132
144
  private
133
145
 
134
- def trace(callable, trace_key, resource, **kwargs)
146
+ # Traces the given callable with the given trace key, resource, and kwargs.
147
+ #
148
+ # @param callable [Proc] the original method call
149
+ # @param trace_key [String] the sub-operation name (`"graphql.#{trace_key}"`)
150
+ # @param resource [String] the resource name for the trace
151
+ # @param before [Proc, nil] a callable to run before the trace, same as the block parameter
152
+ # @param after [Proc, nil] a callable to run after the trace, which has access to query values after execution
153
+ # @param kwargs [Hash] the arguments to pass to `prepare_span`
154
+ # @yield [Span] the block to run before the trace, same as the `before` parameter
155
+ def trace(callable, trace_key, resource, before = nil, after = nil, **kwargs, &before_block)
135
156
  config = Datadog.configuration.tracing[:graphql]
136
157
 
137
158
  Tracing.trace(
@@ -144,11 +165,20 @@ module Datadog
144
165
  Contrib::Analytics.set_sample_rate(span, config[:analytics_sample_rate])
145
166
  end
146
167
 
147
- yield(span) if block_given?
168
+ # A sanity check for us.
169
+ raise 'Please provide either `before` or a block, but not both' if before && before_block
170
+
171
+ if (before_callable = before || before_block)
172
+ before_callable.call(span)
173
+ end
148
174
 
149
175
  prepare_span(trace_key, kwargs, span) if @has_prepare_span
150
176
 
151
- callable.call
177
+ ret = callable.call
178
+
179
+ after.call(span) if after
180
+
181
+ ret
152
182
  end
153
183
  end
154
184
 
@@ -163,6 +193,67 @@ module Datadog
163
193
  operations
164
194
  end
165
195
  end
196
+
197
+ # Create a Span Event for each error that occurs at query level.
198
+ #
199
+ # These are represented in the Datadog App as special GraphQL errors,
200
+ # given their event name `dd.graphql.query.error`.
201
+ def add_query_error_events(span, errors)
202
+ capture_extensions = Datadog.configuration.tracing[:graphql][:error_extensions]
203
+ errors.each do |error|
204
+ extensions = if !capture_extensions.empty? && (extensions = error.extensions)
205
+ # Capture extensions, ensuring all values are primitives
206
+ extensions.each_with_object({}) do |(key, value), hash|
207
+ next unless capture_extensions.include?(key.to_s)
208
+
209
+ value = case value
210
+ when TrueClass, FalseClass, Integer, Float
211
+ value
212
+ else
213
+ # Stringify anything that is not a boolean or a number
214
+ value.to_s
215
+ end
216
+
217
+ hash["extensions.#{key}"] = value
218
+ end
219
+ else
220
+ {}
221
+ end
222
+
223
+ # {::GraphQL::Error#to_h} returns the error formatted in compliance with the GraphQL spec.
224
+ # This is an unwritten contract in the `graphql` library.
225
+ # See for an example: https://github.com/rmosolgo/graphql-ruby/blob/0afa241775e5a113863766cce126214dee093464/lib/graphql/execution_error.rb#L32
226
+ graphql_error = error.to_h
227
+ error = Core::Error.build_from(error)
228
+
229
+ span.span_events << Datadog::Tracing::SpanEvent.new(
230
+ Ext::EVENT_QUERY_ERROR,
231
+ attributes: extensions.merge!(
232
+ message: graphql_error['message'],
233
+ type: error.type,
234
+ stacktrace: error.backtrace,
235
+ locations: serialize_error_locations(graphql_error['locations']),
236
+ path: graphql_error['path'],
237
+ )
238
+ )
239
+ end
240
+ end
241
+
242
+ # Serialize error's `locations` array as an array of Strings, given
243
+ # Span Events do not support hashes nested inside arrays.
244
+ #
245
+ # Here's an example in which `locations`:
246
+ # [
247
+ # {"line" => 3, "column" => 10},
248
+ # {"line" => 7, "column" => 8},
249
+ # ]
250
+ # is serialized as:
251
+ # ["3:10", "7:8"]
252
+ def serialize_error_locations(locations)
253
+ locations.map do |location|
254
+ "#{location['line']}:#{location['column']}"
255
+ end
256
+ end
166
257
  end
167
258
  end
168
259
  end
@@ -31,7 +31,17 @@ module Datadog
31
31
  end
32
32
 
33
33
  def self.to_rack_header(name)
34
- "HTTP_#{name.to_s.upcase.gsub(/[-\s]/, '_')}"
34
+ key = name.to_s.upcase.gsub(/[-\s]/, '_')
35
+ case key
36
+ when 'CONTENT_TYPE', 'CONTENT_LENGTH'
37
+ # NOTE: The Rack spec says:
38
+ # > The environment must not contain the keys HTTP_CONTENT_TYPE or HTTP_CONTENT_LENGTH
39
+ # > (use the versions without HTTP_).
40
+ # See https://github.com/rack/rack/blob/e217a399eb116362710aac7c5b8dc691ea2189b3/SPEC.rdoc?plain=1#L119-L121
41
+ key
42
+ else
43
+ "HTTP_#{key}"
44
+ end
35
45
  end
36
46
  end
37
47
  end
@@ -43,7 +43,7 @@ module Datadog
43
43
  # so that all spans will be added to the distributed trace.
44
44
  if configuration[:distributed_tracing]
45
45
  trace_digest = Contrib::HTTP.extract(env)
46
- Tracing.continue_trace!(trace_digest)
46
+ Tracing.continue_trace!(trace_digest) if trace_digest
47
47
  end
48
48
 
49
49
  TraceProxyMiddleware.call(env, configuration) do
@@ -21,9 +21,14 @@ module Datadog
21
21
  end
22
22
 
23
23
  def self.set_peer_service!(span, sources)
24
+ config = Datadog.configuration.tracing.contrib
25
+
26
+ # If `peer_service_defaults` is disabled, we only read peer service from an explicitly set `peer.service` tag
27
+ sources = Datadog::Tracing::Contrib::SpanAttributeSchema::REFLEXIVE_SOURCES unless config.peer_service_defaults
28
+
24
29
  # Acquire all peer.service values as well as any potential remapping
25
30
  peer_service_val, peer_service_source = set_peer_service_from_source(span, sources)
26
- remap_val = Datadog.configuration.tracing.contrib.peer_service_mapping[peer_service_val]
31
+ remap_val = config.peer_service_mapping[peer_service_val]
27
32
 
28
33
  # Only continue to setting peer.service if actual source is found
29
34
  return false unless peer_service_source
@@ -3,7 +3,8 @@
3
3
  require_relative '../../../core/encoding'
4
4
 
5
5
  require_relative '../../../core/transport/http/api/map'
6
- require_relative 'api/spec'
6
+ require_relative '../../../core/transport/http/api/instance'
7
+ require_relative '../../../core/transport/http/api/spec'
7
8
 
8
9
  require_relative 'traces'
9
10
 
@@ -20,7 +21,7 @@ module Datadog
20
21
  module_function
21
22
 
22
23
  def defaults
23
- Datadog::Core::Transport::HTTP::API::Map[
24
+ Core::Transport::HTTP::API::Map[
24
25
  V4 => Spec.new do |s|
25
26
  s.traces = Traces::API::Endpoint.new(
26
27
  '/v0.4/traces',
@@ -36,6 +37,14 @@ module Datadog
36
37
  end,
37
38
  ].with_fallbacks(V4 => V3)
38
39
  end
40
+
41
+ class Instance < Core::Transport::HTTP::API::Instance
42
+ include Traces::API::Instance
43
+ end
44
+
45
+ class Spec < Core::Transport::HTTP::API::Spec
46
+ include Traces::API::Spec
47
+ end
39
48
  end
40
49
  end
41
50
  end
@@ -6,7 +6,6 @@ require_relative '../traces'
6
6
  require_relative 'client'
7
7
  require_relative '../../../core/transport/http/response'
8
8
  require_relative '../../../core/transport/http/api/endpoint'
9
- require_relative 'api/instance'
10
9
 
11
10
  module Datadog
12
11
  module Tracing
@@ -143,8 +142,6 @@ module Datadog
143
142
 
144
143
  # Add traces behavior to transport components
145
144
  HTTP::Client.include(Traces::Client)
146
- HTTP::API::Spec.include(Traces::API::Spec)
147
- HTTP::API::Instance.include(Traces::API::Instance)
148
145
  end
149
146
  end
150
147
  end