datadog 2.45.0 → 2.46.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -2
  3. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -0
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  5. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  6. data/ext/libdatadog_api/di.c +30 -5
  7. data/ext/libdatadog_api/extconf.rb +2 -0
  8. data/ext/libdatadog_api/otel_thread_context.c +20 -14
  9. data/ext/libdatadog_api/process_discovery.c +15 -1
  10. data/ext/libdatadog_api/trace_exporter.c +105 -25
  11. data/ext/libdatadog_extconf_helpers.rb +1 -1
  12. data/lib/datadog/core/configuration/components.rb +29 -8
  13. data/lib/datadog/core/configuration/supported_configurations.rb +5 -2
  14. data/lib/datadog/core/process_discovery.rb +5 -1
  15. data/lib/datadog/core/remote/client/capabilities.rb +34 -29
  16. data/lib/datadog/core/remote/component.rb +28 -12
  17. data/lib/datadog/core/remote/dispatcher.rb +13 -3
  18. data/lib/datadog/di/boot.rb +1 -0
  19. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  20. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  21. data/lib/datadog/di/component.rb +38 -3
  22. data/lib/datadog/di/configuration.rb +53 -2
  23. data/lib/datadog/di/context.rb +7 -2
  24. data/lib/datadog/di/contrib/active_record.rb +8 -2
  25. data/lib/datadog/di/evaluation_failure.rb +28 -0
  26. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  27. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  28. data/lib/datadog/di/instrumenter.rb +65 -15
  29. data/lib/datadog/di/logger.rb +30 -8
  30. data/lib/datadog/di/probe_manager.rb +48 -5
  31. data/lib/datadog/di/probe_notification_builder.rb +28 -26
  32. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  33. data/lib/datadog/di/serializer.rb +130 -54
  34. data/lib/datadog/di/snapshot_event.rb +38 -0
  35. data/lib/datadog/di/transport/http.rb +13 -3
  36. data/lib/datadog/di/transport/input.rb +32 -7
  37. data/lib/datadog/open_feature/activation.rb +268 -0
  38. data/lib/datadog/open_feature/component.rb +31 -15
  39. data/lib/datadog/open_feature/provider.rb +282 -5
  40. data/lib/datadog/open_feature.rb +31 -1
  41. data/lib/datadog/tracing/component.rb +14 -0
  42. data/lib/datadog/tracing/configuration/settings.rb +3 -1
  43. data/lib/datadog/tracing/otel_thread_context.rb +3 -5
  44. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  45. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  46. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  47. data/lib/datadog/tracing/transport/native.rb +16 -1
  48. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  49. data/lib/datadog/tracing/transport/traces.rb +82 -15
  50. data/lib/datadog/version.rb +1 -1
  51. metadata +16 -7
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ext"
4
+ require_relative "../open_feature"
4
5
  require_relative "../core/utils/time"
5
6
  require "open_feature/sdk"
6
7
 
7
8
  module Datadog
8
9
  module OpenFeature
9
- # OpenFeature feature flagging provider backed by Datadog Remote Configuration.
10
+ # OpenFeature feature flagging provider backed by Datadog configuration delivery.
10
11
  #
11
12
  # Requires openfeature-sdk >= 0.5.1 for flag evaluation metrics and EVP hook support.
12
13
  #
@@ -24,13 +25,14 @@ module Datadog
24
25
  #
25
26
  # Example:
26
27
  #
27
- # Make sure to enable Remote Configuration and OpenFeature in the Datadog configuration.
28
+ # Feature Flags use agentless configuration delivery by default. To use
29
+ # Remote Configuration delivery instead, configure the source explicitly.
28
30
  #
29
31
  # ```ruby
30
32
  # # FILE: initializers/datadog.rb
31
33
  # Datadog.configure do |config|
32
34
  # config.remote.enabled = true
33
- # config.open_feature.enabled = true
35
+ # config.feature_flags.configuration_source = "remote_config"
34
36
  # end
35
37
  # ```
36
38
  #
@@ -58,20 +60,110 @@ module Datadog
58
60
  # # => 'Welcome back!'
59
61
  # ```
60
62
  class Provider
63
+ include ::OpenFeature::SDK::Provider::EventEmitter
64
+
61
65
  NAME = "Datadog Feature Flagging Provider"
66
+ INITIALIZATION_CANCELLED_MESSAGE = "Feature Flags provider initialization was cancelled by shutdown"
62
67
 
63
68
  attr_reader :metadata
64
69
 
65
70
  def initialize
66
71
  @metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: NAME).freeze
72
+ @initialization_mutex = Mutex.new
73
+ @initializing = false
74
+ @initialization_thread = nil
75
+ @awaiting_ready_after_fork = false
76
+ @initialization_failed = false
77
+ @provider_error_observed = false
78
+ @ready_pending = false
79
+ @ready_dispatch_claimed = false
80
+ @stale_emitted = false
81
+ @stale_pending = false
82
+ @error_handler = nil
83
+ @initialization_ready_handler = nil
84
+ @waiting_component = nil
85
+ @shutdown = false
67
86
  end
68
87
 
69
88
  def init
70
- # no-op
89
+ @initialization_mutex.synchronize do
90
+ raise INITIALIZATION_CANCELLED_MESSAGE if @shutdown
91
+
92
+ @initializing = true
93
+ @initialization_thread = Thread.current
94
+ @awaiting_ready_after_fork = false
95
+ @initialization_failed = false
96
+ @provider_error_observed = false
97
+ @ready_pending = false
98
+ @ready_dispatch_claimed = false
99
+ @stale_emitted = false
100
+ @stale_pending = false
101
+ end
102
+ ready_handler_installed = install_initialization_ready_handler
103
+
104
+ component, failure = OpenFeature.activate_provider(self)
105
+ cancel_initialization_if_shutdown!
106
+ fail_initialization(component, failure || "Feature Flags component could not be activated") unless component
107
+
108
+ @initialization_mutex.synchronize do
109
+ raise INITIALIZATION_CANCELLED_MESSAGE if @shutdown
110
+
111
+ @waiting_component = component
112
+ end
113
+ # Shutdown publishes this flag through the component lock; taking our mutex here would invert lock order.
114
+ wait_result = component.wait_for_configuration { @shutdown }
115
+ cancel_initialization_if_shutdown!
116
+
117
+ case wait_result
118
+ when Component::CONFIGURATION_READY
119
+ @initialization_mutex.synchronize { @initializing = false } unless ready_handler_installed
120
+ when Component::CONFIGURATION_TIMEOUT
121
+ message = "Feature Flags provider initialization timed out while waiting for configuration"
122
+ Datadog.logger.error(message)
123
+ fail_initialization(component, message)
124
+ else
125
+ fail_initialization(component, "Feature Flags provider initialization stopped before configuration arrived")
126
+ end
127
+ rescue
128
+ prepare_initialization_failure(component) if @initialization_mutex.synchronize { @initializing }
129
+ raise
130
+ ensure
131
+ @initialization_mutex.synchronize do
132
+ @waiting_component = nil
133
+ # The SDK still owes READY after init returns; the initializing thread owns that dispatch.
134
+ @initialization_thread = nil unless @initializing
135
+ end
71
136
  end
72
137
 
73
138
  def shutdown
74
- # no-op
139
+ configuration = @configuration
140
+ # @type var error_handler: Provider::provider_event_handler?
141
+ error_handler = nil
142
+ # @type var initialization_ready_handler: Provider::provider_event_handler?
143
+ initialization_ready_handler = nil
144
+ waiting_component = nil #: Component?
145
+ @initialization_mutex.synchronize do
146
+ return if @shutdown
147
+
148
+ @shutdown = true
149
+ @initializing = false
150
+ @initialization_thread = nil
151
+ error_handler = @error_handler
152
+ initialization_ready_handler = @initialization_ready_handler
153
+ waiting_component = @waiting_component
154
+ @error_handler = nil
155
+ @initialization_ready_handler = nil
156
+ end
157
+
158
+ waiting_component&.wake_configuration_waiters
159
+ configuration&.remove_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, error_handler) if error_handler
160
+ if initialization_ready_handler
161
+ configuration&.remove_handler(
162
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_READY,
163
+ initialization_ready_handler,
164
+ )
165
+ end
166
+ OpenFeature.deactivate_provider(self)
75
167
  end
76
168
 
77
169
  def hooks
@@ -108,6 +200,191 @@ module Datadog
108
200
 
109
201
  private
110
202
 
203
+ def after_fork(component)
204
+ handler = @initialization_mutex.synchronize do
205
+ return if @shutdown || !@initializing || @initialization_thread&.alive?
206
+
207
+ @initializing = false
208
+ @initialization_thread = nil
209
+ @waiting_component = nil
210
+ @awaiting_ready_after_fork = true
211
+ @stale_pending = false
212
+ previous_handler = @initialization_ready_handler
213
+ @initialization_ready_handler = nil
214
+ previous_handler
215
+ end
216
+ if handler
217
+ @configuration&.remove_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY, handler)
218
+ end
219
+ configuration_changed(Component::CONFIGURATION_READY) if component&.configuration_received?
220
+ end
221
+
222
+ def fail_initialization(component, message)
223
+ prepare_initialization_failure(component)
224
+ cancel_initialization_if_shutdown!
225
+ raise message.to_s
226
+ end
227
+
228
+ def prepare_initialization_failure(component)
229
+ cancelled, initialization_ready_handler = @initialization_mutex.synchronize do
230
+ handler = @initialization_ready_handler
231
+ @initialization_ready_handler = nil
232
+ @stale_pending = false
233
+ @initializing = false
234
+ @initialization_thread = nil
235
+ if @shutdown
236
+ [true, handler]
237
+ else
238
+ @initialization_failed = true
239
+ @ready_pending ||= !!component&.configuration_received?
240
+ [false, handler]
241
+ end
242
+ end
243
+ if initialization_ready_handler
244
+ @configuration&.remove_handler(
245
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_READY,
246
+ initialization_ready_handler,
247
+ )
248
+ end
249
+ return if cancelled
250
+
251
+ install_error_handler
252
+ end
253
+
254
+ def cancel_initialization_if_shutdown!
255
+ cancelled = @initialization_mutex.synchronize do
256
+ @initializing = false if @shutdown
257
+ @shutdown
258
+ end
259
+ raise INITIALIZATION_CANCELLED_MESSAGE if cancelled
260
+ end
261
+
262
+ def shutdown?
263
+ @initialization_mutex.synchronize { @shutdown }
264
+ end
265
+
266
+ def install_initialization_ready_handler
267
+ configuration = @configuration
268
+ return false unless configuration
269
+
270
+ handler = @initialization_mutex.synchronize do
271
+ return false if @shutdown
272
+ return true if @initialization_ready_handler
273
+
274
+ @initialization_ready_handler = ->(details) { provider_ready(details) }
275
+ end
276
+ configuration.add_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY, handler)
277
+
278
+ cancelled = @initialization_mutex.synchronize { @shutdown }
279
+ configuration.remove_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY, handler) if cancelled
280
+ return false if cancelled
281
+
282
+ true
283
+ end
284
+
285
+ def install_error_handler
286
+ configuration = @configuration
287
+ return unless configuration
288
+
289
+ handler = @initialization_mutex.synchronize do
290
+ return if @shutdown
291
+
292
+ @error_handler ||= ->(details) { provider_error(details) }
293
+ end
294
+ configuration.add_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler)
295
+
296
+ cancelled = @initialization_mutex.synchronize { @shutdown }
297
+ configuration.remove_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, handler) if cancelled
298
+ end
299
+
300
+ def provider_error(_details)
301
+ configuration = @configuration
302
+ return unless configuration
303
+ return unless configuration.send(:provider_state, self) == ::OpenFeature::SDK::ProviderState::ERROR
304
+
305
+ emit_ready = @initialization_mutex.synchronize do
306
+ if @shutdown
307
+ false
308
+ else
309
+ @provider_error_observed = true
310
+ if @ready_pending && !@ready_dispatch_claimed
311
+ @ready_dispatch_claimed = true
312
+ else
313
+ false
314
+ end
315
+ end
316
+ end
317
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY) if emit_ready
318
+ end
319
+
320
+ def provider_ready(_details)
321
+ configuration = @configuration
322
+ return unless configuration
323
+ return unless configuration.send(:provider_state, self) == ::OpenFeature::SDK::ProviderState::READY
324
+
325
+ handler, emit_stale = @initialization_mutex.synchronize do
326
+ handler = @initialization_ready_handler
327
+ @initialization_ready_handler = nil
328
+ @initializing = false
329
+ @initialization_thread = nil
330
+ if @stale_pending && !@shutdown
331
+ @stale_pending = false
332
+ @stale_emitted = true
333
+ [handler, true]
334
+ else
335
+ [handler, false]
336
+ end
337
+ end
338
+ configuration.remove_handler(::OpenFeature::SDK::ProviderEvent::PROVIDER_READY, handler) if handler
339
+ emit_event(::OpenFeature::SDK::ProviderEvent::PROVIDER_STALE) if emit_stale
340
+ end
341
+
342
+ def configuration_changed(event)
343
+ event_type = @initialization_mutex.synchronize do
344
+ if @shutdown
345
+ nil
346
+ elsif event == Component::CONFIGURATION_READY
347
+ if @initializing
348
+ @ready_pending = true
349
+ @stale_pending = false
350
+ nil
351
+ elsif @awaiting_ready_after_fork
352
+ @awaiting_ready_after_fork = false
353
+ @stale_emitted = false
354
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_READY
355
+ elsif @initialization_failed && !@provider_error_observed
356
+ @stale_emitted = false
357
+ @ready_pending = true
358
+ nil
359
+ elsif @stale_emitted
360
+ @stale_emitted = false
361
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_READY
362
+ elsif @initialization_failed
363
+ if @provider_error_observed && !@ready_dispatch_claimed
364
+ @ready_dispatch_claimed = true
365
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_READY
366
+ else
367
+ @ready_pending = true
368
+ nil
369
+ end
370
+ end
371
+ elsif event == Component::CONFIGURATION_CHANGED && !@initializing
372
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_CONFIGURATION_CHANGED
373
+ elsif event == Component::CONFIGURATION_LOST
374
+ @ready_pending = false
375
+ if @initializing
376
+ @stale_pending = true
377
+ nil
378
+ else
379
+ @stale_emitted = true
380
+ ::OpenFeature::SDK::ProviderEvent::PROVIDER_STALE
381
+ end
382
+ end
383
+ end
384
+
385
+ emit_event(event_type) if event_type
386
+ end
387
+
111
388
  def evaluate(flag_key, default_value:, expected_type:, evaluation_context:)
112
389
  # Stamp evaluation entry time once, here on the eval thread. The EVP path uses this for
113
390
  # accurate first/last_evaluation bounds instead of a later hook-fire clock read.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "core/configuration"
4
4
  require_relative "open_feature/configuration"
5
+ require_relative "open_feature/configuration/source"
5
6
 
6
7
  module Datadog
7
8
  # A namespace for the OpenFeature component.
@@ -9,11 +10,40 @@ module Datadog
9
10
  Core::Configuration::Settings.extend(Configuration::Settings)
10
11
 
11
12
  def self.enabled?
12
- Datadog.configuration.open_feature.enabled
13
+ Configuration::Source.resolve(Datadog.configuration).enabled?
13
14
  end
14
15
 
15
16
  def self.engine
16
17
  Datadog.send(:components).open_feature&.engine
17
18
  end
19
+
20
+ def self.activate_provider(provider)
21
+ # Initialize the component tree before taking its reconfiguration lock.
22
+ Datadog.send(:components)
23
+ Datadog.send(:safely_synchronize) do
24
+ return [nil, nil] if provider.send(:shutdown?)
25
+
26
+ components = Datadog.send(:components, allow_initialization: false)
27
+ activation = components&.send(:open_feature_activation)
28
+ providers = @adopted_providers ||= {}.compare_by_identity
29
+ providers[provider] = true
30
+ [activation&.activate(provider), activation&.failure]
31
+ end
32
+ end
33
+
34
+ def self.deactivate_provider(provider)
35
+ Datadog.send(:safely_synchronize) do
36
+ @adopted_providers&.delete(provider)
37
+ components = Datadog.send(:components, allow_initialization: false)
38
+ components&.send(:open_feature_activation)&.deactivate(provider)
39
+ end
40
+ end
41
+
42
+ # Components startup already holds the non-reentrant reconfiguration lock.
43
+ def self.reattach(activation)
44
+ providers = @adopted_providers&.keys || []
45
+ providers.each { |provider| activation.activate(provider) }
46
+ nil
47
+ end
18
48
  end
19
49
  end
@@ -62,6 +62,20 @@ module Datadog
62
62
  end
63
63
  end
64
64
 
65
+ def bind_transport_telemetry(tracer, telemetry)
66
+ return unless tracer.is_a?(Tracing::Tracer)
67
+
68
+ writer = tracer.writer
69
+ return unless writer.respond_to?(:transport)
70
+
71
+ transport = writer.transport
72
+ if transport.is_a?(Transport::Traces::Transport) ||
73
+ (defined?(Transport::Native::Transport) && transport.is_a?(Transport::Native::Transport))
74
+ transport.telemetry = telemetry
75
+ end
76
+ nil
77
+ end
78
+
65
79
  def build_sampler(settings)
66
80
  # A custom sampler is provided
67
81
  if (sampler = settings.tracing.sampler)
@@ -258,7 +258,9 @@ module Datadog
258
258
 
259
259
  option :otel_thread_context_enabled do |o|
260
260
  o.env Tracing::Configuration::Ext::ENV_OTEL_CONTEXT_ENABLED
261
- o.default true
261
+ o.default do
262
+ RUBY_ENGINE == "ruby" && RUBY_PLATFORM.include?("linux")
263
+ end
262
264
  o.type :bool
263
265
  end
264
266
 
@@ -14,9 +14,11 @@ module Datadog
14
14
 
15
15
  def self.build(tracing_settings)
16
16
  return unless tracing_settings.otel_thread_context_enabled
17
+ return unless Datadog::Core::LIBDATADOG_API_FAILURE.nil?
17
18
 
18
19
  instance = new
19
- instance if instance.supported? && instance._native_enable
20
+ instance._native_enable
21
+ instance
20
22
  end
21
23
 
22
24
  def initialize
@@ -65,10 +67,6 @@ module Datadog
65
67
  false
66
68
  end
67
69
 
68
- def supported?
69
- Datadog::Core::LIBDATADOG_API_FAILURE.nil? && _native_supported?
70
- end
71
-
72
70
  def update_from_trace_op(trace_op)
73
71
  active_span = trace_op.active_span
74
72
 
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "../telemetry"
5
+
6
+ module Datadog
7
+ module Tracing
8
+ module Transport
9
+ module HTTP
10
+ class Telemetry < Datadog::Tracing::Transport::Telemetry
11
+ def initialize(client)
12
+ super(client, source: "ruby")
13
+ end
14
+
15
+ def batch(traces)
16
+ return unless @client.enabled? && @client.metrics_manager.enabled
17
+
18
+ Batch.new(self, traces)
19
+ rescue
20
+ nil
21
+ end
22
+
23
+ class Batch
24
+ def initialize(reporter, traces)
25
+ @reporter = reporter
26
+ @remaining_chunks = traces.length
27
+ @remaining_spans = traces.sum(&:length)
28
+ @reporter.record(spans_enqueued_for_serialization: @remaining_spans)
29
+ end
30
+
31
+ def record_oversized_drop(span_count)
32
+ @remaining_chunks -= 1
33
+ @remaining_spans -= span_count
34
+ @reporter.record(chunks_dropped_payload_too_large: 1, spans_dropped_payload_too_large: span_count)
35
+ end
36
+
37
+ def serialization_failed
38
+ @reporter.record(chunks_dropped_serialization_error: @remaining_chunks,
39
+ spans_dropped_serialization_error: @remaining_spans,)
40
+ end
41
+
42
+ def sent(request, response, chunks, spans, bytes, fallback:)
43
+ @reporter.record(requests_count: 1)
44
+ return if fallback
45
+
46
+ @remaining_chunks -= chunks
47
+ @remaining_spans -= spans
48
+ observations = {} #: Hash[Symbol, Integer]
49
+ if (status = request.http_status)
50
+ observations[:responses_count] = 1
51
+ observations[:status_code] = status
52
+ end
53
+ success = status ? status.between?(200, 299) : request.http_success
54
+ if success
55
+ observations[:chunks_sent] = chunks
56
+ observations[:bytes_sent] = bytes
57
+ elsif status || success == false
58
+ observations[:errors_status_code] = 1 if status
59
+ observations[:chunks_dropped_send_failure] = chunks
60
+ observations[:spans_dropped_api_error] = spans
61
+ elsif response.is_a?(Core::Transport::InternalErrorResponse)
62
+ error = response.error
63
+ if error.is_a?(ArgumentError) || error.is_a?(TypeError)
64
+ observations[:chunks_dropped_serialization_error] = chunks
65
+ observations[:spans_dropped_serialization_error] = spans
66
+ else
67
+ observations[:chunks_dropped_send_failure] = chunks
68
+ observations[:spans_dropped_api_error] = spans
69
+ if error.is_a?(::Timeout::Error) || error.is_a?(Errno::ETIMEDOUT)
70
+ observations[:errors_timeout] = 1
71
+ elsif network_error?(error)
72
+ observations[:errors_network] = 1
73
+ end
74
+ end
75
+ end
76
+ @reporter.record(observations)
77
+ rescue
78
+ nil
79
+ end
80
+
81
+ private
82
+
83
+ def network_error?(error)
84
+ error.is_a?(SystemCallError) || error.is_a?(IOError) || error.is_a?(SocketError) ||
85
+ error.is_a?(::Net::ProtocolError) || error.is_a?(::Net::HTTPBadResponse) ||
86
+ error.is_a?(::Net::HTTPHeaderSyntaxError) ||
87
+ (defined?(::OpenSSL::SSL::SSLError) && error.is_a?(::OpenSSL::SSL::SSLError))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -51,6 +51,8 @@ module Datadog
51
51
 
52
52
  # Query for response
53
53
  http_response = super
54
+ # Delivery succeeded or failed before service-rate parsing can raise.
55
+ env.request.capture_status(http_response)
54
56
 
55
57
  # Process the response
56
58
  response_options = {trace_count: env.request.parcel.trace_count}.tap do |options|
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../telemetry"
4
+
5
+ module Datadog
6
+ module Tracing
7
+ module Transport
8
+ module Native
9
+ class Telemetry < Datadog::Tracing::Transport::Telemetry
10
+ def initialize(client)
11
+ super(client, source: "libdatadog")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,6 +4,7 @@ require "json"
4
4
  require_relative "trace_formatter"
5
5
  require_relative "statistics"
6
6
  require_relative "span_events_negotiation"
7
+ require_relative "native/telemetry"
7
8
 
8
9
  module Datadog
9
10
  module Tracing
@@ -49,6 +50,16 @@ module Datadog
49
50
 
50
51
  attr_reader :logger, :url
51
52
 
53
+ def telemetry=(client)
54
+ @send_mutex.synchronize do
55
+ if (reporter = @telemetry)
56
+ reporter.client = client
57
+ elsif @exporter
58
+ @telemetry = Telemetry.new(client)
59
+ end
60
+ end
61
+ end
62
+
52
63
  # @param agent_settings [Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings]
53
64
  # Agent connection settings, including the request timeout.
54
65
  # @param logger [Logger]
@@ -58,6 +69,7 @@ module Datadog
58
69
  end
59
70
 
60
71
  @logger = logger
72
+ @telemetry = nil
61
73
 
62
74
  # Serializes native sends and is held across a fork. See the
63
75
  # fork-safety note below.
@@ -226,6 +238,7 @@ module Datadog
226
238
  @exporter&._native_close
227
239
  ensure
228
240
  @fork_hooks = nil
241
+ @telemetry = nil
229
242
  @exporter = nil
230
243
  hooks.each do |stage, block|
231
244
  Core::Utils::AtForkMonkeyPatch.remove_at_fork(stage, block)
@@ -285,7 +298,9 @@ module Datadog
285
298
  exporter = @exporter
286
299
  raise "Native transport has been closed" if exporter.nil?
287
300
 
288
- exporter._native_send_traces(chunks, native_events_supported)
301
+ exporter._native_send_traces(chunks, native_events_supported) do |observations|
302
+ @telemetry&.record(observations)
303
+ end
289
304
  end
290
305
 
291
306
  # Update statistics from the response
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Tracing
5
+ module Transport
6
+ class Telemetry
7
+ # Observation schema: Symbol keys with non-negative Integer values.
8
+ # COUNTS maps per-send count deltas to [metric name, additional tags].
9
+ # :bytes_sent is one successful payload's byte-size distribution sample;
10
+ # record each payload separately rather than summing its samples.
11
+ # :responses_count requires :status_code; omit both when the code is unknown.
12
+ # Missing and zero measurements emit nothing. All metrics use namespace
13
+ # "tracers" and common: true; trace_* metrics gain src_library:<source>, while
14
+ # span metrics keep their source-free tags.
15
+ COUNTS = {
16
+ requests_count: ["trace_api.requests", []],
17
+ errors_network: ["trace_api.errors", ["type:network"]],
18
+ errors_timeout: ["trace_api.errors", ["type:timeout"]],
19
+ errors_status_code: ["trace_api.errors", ["type:status_code"]],
20
+ chunks_sent: ["trace_chunks_sent", []],
21
+ chunks_dropped_serialization_error: ["trace_chunks_dropped", ["reason:serialization_error"]],
22
+ chunks_dropped_send_failure: ["trace_chunks_dropped", ["reason:send_failure"]],
23
+ chunks_dropped_payload_too_large: ["trace_chunks_dropped", ["reason:payload_too_large"]],
24
+ spans_enqueued_for_serialization: ["spans_enqueued_for_serialization", []],
25
+ spans_dropped_serialization_error: ["spans_dropped", ["reason:serialization_error"]],
26
+ spans_dropped_api_error: ["spans_dropped", ["reason:api_error"]],
27
+ spans_dropped_payload_too_large: ["spans_dropped", ["reason:payload_too_large"]],
28
+ }.freeze #: Hash[Symbol, [String, Array[String]]]
29
+
30
+ attr_writer :client
31
+
32
+ def initialize(client, source:)
33
+ @client = client
34
+ @source_tags = ["src_library:#{source}"]
35
+ @counts = COUNTS.map do |key, (name, tags)|
36
+ [key, name, name.start_with?("trace_") ? @source_tags + tags : tags]
37
+ end
38
+ end
39
+
40
+ def record(observations)
41
+ client = @client
42
+ return unless client.enabled?
43
+
44
+ @counts.each do |key, name, tags|
45
+ value = observations.fetch(key, 0)
46
+ client.inc("tracers", name, value, tags: tags) if value > 0
47
+ end
48
+ bytes = observations.fetch(:bytes_sent, 0)
49
+ client.distribution("tracers", "trace_api.bytes", bytes, tags: @source_tags) if bytes > 0
50
+ responses = observations.fetch(:responses_count, 0)
51
+ if responses > 0
52
+ client.inc("tracers", "trace_api.responses", responses,
53
+ tags: @source_tags + ["status_code:#{observations.fetch(:status_code)}"],)
54
+ end
55
+ nil
56
+ rescue
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end