datadog 2.22.0 → 2.24.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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -1
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +1 -58
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +21 -5
  5. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +239 -0
  6. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +1 -1
  7. data/ext/datadog_profiling_native_extension/extconf.rb +9 -4
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
  9. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +12 -0
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +4 -0
  11. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  12. data/ext/libdatadog_api/datadog_ruby_common.h +1 -1
  13. data/ext/libdatadog_api/feature_flags.c +554 -0
  14. data/ext/libdatadog_api/feature_flags.h +5 -0
  15. data/ext/libdatadog_api/init.c +2 -0
  16. data/ext/libdatadog_api/library_config.c +12 -11
  17. data/ext/libdatadog_extconf_helpers.rb +1 -1
  18. data/lib/datadog/appsec/api_security/route_extractor.rb +23 -6
  19. data/lib/datadog/appsec/api_security/sampler.rb +7 -4
  20. data/lib/datadog/appsec/assets/blocked.html +8 -0
  21. data/lib/datadog/appsec/assets/blocked.json +1 -1
  22. data/lib/datadog/appsec/assets/blocked.text +3 -1
  23. data/lib/datadog/appsec/assets.rb +1 -1
  24. data/lib/datadog/appsec/context.rb +2 -1
  25. data/lib/datadog/appsec/remote.rb +5 -9
  26. data/lib/datadog/appsec/response.rb +18 -4
  27. data/lib/datadog/appsec/security_engine/result.rb +2 -1
  28. data/lib/datadog/core/configuration/components.rb +30 -3
  29. data/lib/datadog/core/configuration/config_helper.rb +2 -2
  30. data/lib/datadog/core/configuration/deprecations.rb +2 -2
  31. data/lib/datadog/core/configuration/option_definition.rb +4 -2
  32. data/lib/datadog/core/configuration/options.rb +8 -5
  33. data/lib/datadog/core/configuration/settings.rb +28 -3
  34. data/lib/datadog/core/configuration/supported_configurations.rb +332 -302
  35. data/lib/datadog/core/ddsketch.rb +0 -2
  36. data/lib/datadog/core/environment/cgroup.rb +52 -25
  37. data/lib/datadog/core/environment/container.rb +140 -46
  38. data/lib/datadog/core/environment/ext.rb +7 -0
  39. data/lib/datadog/core/environment/process.rb +87 -0
  40. data/lib/datadog/core/feature_flags.rb +61 -0
  41. data/lib/datadog/core/rate_limiter.rb +9 -1
  42. data/lib/datadog/core/remote/client/capabilities.rb +7 -0
  43. data/lib/datadog/core/remote/client.rb +14 -6
  44. data/lib/datadog/core/remote/component.rb +6 -4
  45. data/lib/datadog/core/remote/configuration/content.rb +15 -2
  46. data/lib/datadog/core/remote/configuration/digest.rb +14 -7
  47. data/lib/datadog/core/remote/configuration/repository.rb +1 -1
  48. data/lib/datadog/core/remote/configuration/target.rb +13 -6
  49. data/lib/datadog/core/remote/transport/config.rb +4 -25
  50. data/lib/datadog/core/remote/transport/http/config.rb +10 -50
  51. data/lib/datadog/core/remote/transport/http/negotiation.rb +14 -44
  52. data/lib/datadog/core/remote/transport/http.rb +15 -24
  53. data/lib/datadog/core/remote/transport/negotiation.rb +8 -33
  54. data/lib/datadog/core/remote/worker.rb +25 -37
  55. data/lib/datadog/core/tag_builder.rb +0 -4
  56. data/lib/datadog/core/tag_normalizer.rb +84 -0
  57. data/lib/datadog/core/telemetry/component.rb +59 -16
  58. data/lib/datadog/core/telemetry/event/app_started.rb +86 -49
  59. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +27 -4
  60. data/lib/datadog/core/telemetry/logger.rb +2 -2
  61. data/lib/datadog/core/telemetry/logging.rb +2 -8
  62. data/lib/datadog/core/telemetry/metrics_manager.rb +9 -0
  63. data/lib/datadog/core/telemetry/request.rb +17 -3
  64. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +3 -34
  65. data/lib/datadog/core/telemetry/transport/http.rb +21 -16
  66. data/lib/datadog/core/telemetry/transport/telemetry.rb +3 -11
  67. data/lib/datadog/core/telemetry/worker.rb +88 -32
  68. data/lib/datadog/core/transport/ext.rb +2 -0
  69. data/lib/datadog/core/transport/http/api/endpoint.rb +9 -4
  70. data/lib/datadog/core/transport/http/api/instance.rb +4 -21
  71. data/lib/datadog/core/transport/http/builder.rb +9 -5
  72. data/lib/datadog/core/transport/http/client.rb +80 -0
  73. data/lib/datadog/core/transport/http.rb +22 -19
  74. data/lib/datadog/core/transport/response.rb +9 -0
  75. data/lib/datadog/core/transport/transport.rb +90 -0
  76. data/lib/datadog/core/utils/array.rb +29 -0
  77. data/lib/datadog/{appsec/api_security → core/utils}/lru_cache.rb +10 -21
  78. data/lib/datadog/core/utils/network.rb +3 -1
  79. data/lib/datadog/core/utils/only_once_successful.rb +8 -2
  80. data/lib/datadog/core/utils/time.rb +1 -1
  81. data/lib/datadog/core/utils.rb +2 -0
  82. data/lib/datadog/core/workers/async.rb +10 -1
  83. data/lib/datadog/core/workers/interval_loop.rb +44 -3
  84. data/lib/datadog/core/workers/polling.rb +2 -0
  85. data/lib/datadog/core/workers/queue.rb +100 -1
  86. data/lib/datadog/data_streams/configuration/settings.rb +49 -0
  87. data/lib/datadog/data_streams/configuration.rb +11 -0
  88. data/lib/datadog/data_streams/ext.rb +11 -0
  89. data/lib/datadog/data_streams/extensions.rb +16 -0
  90. data/lib/datadog/data_streams/pathway_context.rb +169 -0
  91. data/lib/datadog/data_streams/processor.rb +509 -0
  92. data/lib/datadog/data_streams/transport/http/stats.rb +52 -0
  93. data/lib/datadog/data_streams/transport/http.rb +40 -0
  94. data/lib/datadog/data_streams/transport/stats.rb +46 -0
  95. data/lib/datadog/data_streams.rb +100 -0
  96. data/lib/datadog/di/component.rb +0 -16
  97. data/lib/datadog/di/contrib/active_record.rb +31 -5
  98. data/lib/datadog/di/el/compiler.rb +8 -4
  99. data/lib/datadog/di/el/evaluator.rb +1 -1
  100. data/lib/datadog/di/error.rb +9 -0
  101. data/lib/datadog/di/instrumenter.rb +93 -34
  102. data/lib/datadog/di/probe.rb +20 -0
  103. data/lib/datadog/di/probe_builder.rb +2 -1
  104. data/lib/datadog/di/probe_manager.rb +47 -33
  105. data/lib/datadog/di/probe_notification_builder.rb +77 -25
  106. data/lib/datadog/di/proc_responder.rb +32 -0
  107. data/lib/datadog/di/remote.rb +89 -84
  108. data/lib/datadog/di/transport/diagnostics.rb +8 -36
  109. data/lib/datadog/di/transport/http/diagnostics.rb +1 -33
  110. data/lib/datadog/di/transport/http/input.rb +1 -33
  111. data/lib/datadog/di/transport/http.rb +32 -17
  112. data/lib/datadog/di/transport/input.rb +67 -34
  113. data/lib/datadog/di.rb +61 -5
  114. data/lib/datadog/open_feature/component.rb +60 -0
  115. data/lib/datadog/open_feature/configuration.rb +27 -0
  116. data/lib/datadog/open_feature/evaluation_engine.rb +70 -0
  117. data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
  118. data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
  119. data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
  120. data/lib/datadog/open_feature/exposures/event.rb +60 -0
  121. data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
  122. data/lib/datadog/open_feature/exposures/worker.rb +116 -0
  123. data/lib/datadog/open_feature/ext.rb +14 -0
  124. data/lib/datadog/open_feature/native_evaluator.rb +38 -0
  125. data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
  126. data/lib/datadog/open_feature/provider.rb +141 -0
  127. data/lib/datadog/open_feature/remote.rb +67 -0
  128. data/lib/datadog/open_feature/resolution_details.rb +35 -0
  129. data/lib/datadog/open_feature/transport.rb +70 -0
  130. data/lib/datadog/open_feature.rb +19 -0
  131. data/lib/datadog/opentelemetry/api/baggage.rb +1 -1
  132. data/lib/datadog/opentelemetry/configuration/settings.rb +159 -0
  133. data/lib/datadog/opentelemetry/metrics.rb +117 -0
  134. data/lib/datadog/opentelemetry/sdk/configurator.rb +25 -1
  135. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +35 -0
  136. data/lib/datadog/opentelemetry.rb +3 -0
  137. data/lib/datadog/profiling/collectors/code_provenance.rb +41 -7
  138. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  139. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -1
  140. data/lib/datadog/profiling/collectors/info.rb +2 -1
  141. data/lib/datadog/profiling/component.rb +12 -11
  142. data/lib/datadog/profiling/http_transport.rb +4 -1
  143. data/lib/datadog/profiling/profiler.rb +4 -0
  144. data/lib/datadog/profiling/tag_builder.rb +36 -3
  145. data/lib/datadog/profiling.rb +1 -2
  146. data/lib/datadog/single_step_instrument.rb +1 -1
  147. data/lib/datadog/tracing/configuration/ext.rb +9 -0
  148. data/lib/datadog/tracing/configuration/settings.rb +74 -0
  149. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -4
  150. data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -2
  151. data/lib/datadog/tracing/contrib/active_job/log_injection.rb +21 -7
  152. data/lib/datadog/tracing/contrib/active_job/patcher.rb +5 -1
  153. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +4 -2
  154. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -1
  155. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +11 -3
  156. data/lib/datadog/tracing/contrib/extensions.rb +10 -2
  157. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +11 -7
  158. data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +7 -3
  159. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +22 -17
  160. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +11 -3
  161. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +11 -3
  162. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +11 -3
  163. data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +66 -0
  164. data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +66 -0
  165. data/lib/datadog/tracing/contrib/kafka/patcher.rb +14 -0
  166. data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
  167. data/lib/datadog/tracing/contrib/karafka/monitor.rb +11 -0
  168. data/lib/datadog/tracing/contrib/karafka/patcher.rb +35 -4
  169. data/lib/datadog/tracing/contrib/rack/middlewares.rb +59 -27
  170. data/lib/datadog/tracing/contrib/rack/route_inference.rb +53 -0
  171. data/lib/datadog/tracing/contrib/rails/middlewares.rb +2 -2
  172. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +4 -1
  173. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +3 -1
  174. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +3 -1
  175. data/lib/datadog/tracing/contrib/status_range_matcher.rb +9 -1
  176. data/lib/datadog/tracing/contrib/utils/quantization/hash.rb +3 -1
  177. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
  178. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
  179. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
  180. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
  181. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
  182. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +49 -0
  183. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
  184. data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
  185. data/lib/datadog/tracing/contrib.rb +1 -0
  186. data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -1
  187. data/lib/datadog/tracing/metadata/ext.rb +1 -1
  188. data/lib/datadog/tracing/remote.rb +1 -9
  189. data/lib/datadog/tracing/span_event.rb +2 -2
  190. data/lib/datadog/tracing/span_operation.rb +9 -4
  191. data/lib/datadog/tracing/trace_operation.rb +44 -6
  192. data/lib/datadog/tracing/tracer.rb +42 -16
  193. data/lib/datadog/tracing/transport/http/client.rb +12 -26
  194. data/lib/datadog/tracing/transport/http/traces.rb +2 -50
  195. data/lib/datadog/tracing/transport/http.rb +15 -9
  196. data/lib/datadog/tracing/transport/io/client.rb +1 -1
  197. data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
  198. data/lib/datadog/tracing/transport/traces.rb +9 -71
  199. data/lib/datadog/tracing/workers/trace_writer.rb +5 -0
  200. data/lib/datadog/tracing/writer.rb +1 -0
  201. data/lib/datadog/version.rb +2 -2
  202. data/lib/datadog.rb +2 -0
  203. metadata +78 -21
  204. data/lib/datadog/core/remote/transport/http/api.rb +0 -53
  205. data/lib/datadog/core/remote/transport/http/client.rb +0 -49
  206. data/lib/datadog/core/telemetry/transport/http/api.rb +0 -43
  207. data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
  208. data/lib/datadog/core/transport/http/api/spec.rb +0 -36
  209. data/lib/datadog/di/transport/http/api.rb +0 -42
  210. data/lib/datadog/di/transport/http/client.rb +0 -47
  211. data/lib/datadog/opentelemetry/api/baggage.rbs +0 -26
  212. data/lib/datadog/tracing/transport/http/api.rb +0 -44
@@ -1,31 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../core/encoding'
4
+ require_relative '../../core/transport/http'
3
5
  require_relative 'diagnostics'
4
6
  require_relative 'input'
5
- require_relative 'http/api'
6
- require_relative '../../core/transport/http'
7
7
 
8
8
  module Datadog
9
9
  module DI
10
10
  module Transport
11
11
  # Namespace for HTTP transport components
12
12
  module HTTP
13
- module_function
13
+ DIAGNOSTICS = Diagnostics::API::Endpoint.new(
14
+ '/debugger/v1/diagnostics',
15
+ Core::Encoding::JSONEncoder,
16
+ )
17
+
18
+ INPUT = Input::API::Endpoint.new(
19
+ '/debugger/v2/input',
20
+ Core::Encoding::JSONEncoder,
21
+ )
22
+
23
+ LEGACY_INPUT = Input::API::Endpoint.new(
24
+ # We used to use /debugger/v1/input, but now input
25
+ # payloads should be going to the diagnostics endpoint
26
+ # which I gather performs data redaction.
27
+ '/debugger/v1/diagnostics',
28
+ Core::Encoding::JSONEncoder,
29
+ )
14
30
 
15
31
  # Builds a new Transport::HTTP::Client with default settings
16
32
  # Pass a block to override any settings.
17
- def diagnostics(
33
+ def self.diagnostics(
18
34
  agent_settings:,
19
35
  logger:,
20
- api_version: nil,
21
36
  headers: nil
22
37
  )
23
- Core::Transport::HTTP.build(api_instance_class: Diagnostics::API::Instance,
38
+ Core::Transport::HTTP.build(
24
39
  logger: logger,
25
- agent_settings: agent_settings, api_version: api_version, headers: headers) do |transport|
26
- apis = API.defaults
27
-
28
- transport.api API::DIAGNOSTICS, apis[API::DIAGNOSTICS]
40
+ agent_settings: agent_settings,
41
+ headers: headers,
42
+ ) do |transport|
43
+ transport.api 'diagnostics', DIAGNOSTICS
29
44
 
30
45
  # Call block to apply any customization, if provided
31
46
  yield(transport) if block_given?
@@ -34,18 +49,18 @@ module Datadog
34
49
 
35
50
  # Builds a new Transport::HTTP::Client with default settings
36
51
  # Pass a block to override any settings.
37
- def input(
52
+ def self.input(
38
53
  agent_settings:,
39
54
  logger:,
40
- api_version: nil,
41
55
  headers: nil
42
56
  )
43
- Core::Transport::HTTP.build(api_instance_class: Input::API::Instance,
57
+ Core::Transport::HTTP.build(
44
58
  logger: logger,
45
- agent_settings: agent_settings, api_version: api_version, headers: headers) do |transport|
46
- apis = API.defaults
47
-
48
- transport.api API::INPUT, apis[API::INPUT]
59
+ agent_settings: agent_settings,
60
+ headers: headers,
61
+ ) do |transport|
62
+ transport.api 'input', INPUT, fallback: 'legacy_input', default: true
63
+ transport.api 'legacy_input', LEGACY_INPUT
49
64
 
50
65
  # Call block to apply any customization, if provided
51
66
  yield(transport) if block_given?
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../core/chunker'
4
+ require_relative '../../core/encoding'
5
+ require_relative '../../core/tag_builder'
3
6
  require_relative '../../core/transport/parcel'
4
- require_relative 'http/client'
7
+ require_relative '../../core/transport/request'
8
+ require_relative '../../core/transport/transport'
9
+ require_relative '../error'
10
+ require_relative 'http/input'
5
11
 
6
12
  module Datadog
7
13
  module DI
@@ -21,47 +27,74 @@ module Datadog
21
27
  end
22
28
  end
23
29
 
24
- class Transport
25
- attr_reader :client, :apis, :default_api, :current_api_id, :logger
30
+ class Transport < Core::Transport::Transport
31
+ # The limit on an individual snapshot payload, aka "log line",
32
+ # is 1 MB.
33
+ #
34
+ # TODO There is an RFC for snapshot pruning that should be
35
+ # implemented to reduce the size of snapshots to be below this
36
+ # limit, so that we can send a portion of the captured data
37
+ # rather than dropping the snapshot entirely.
38
+ MAX_SERIALIZED_SNAPSHOT_SIZE = 1024 * 1024
26
39
 
27
- def initialize(apis, default_api, logger:)
28
- @apis = apis
29
- @logger = logger
40
+ # The maximum chunk (batch) size that intake permits is 5 MB.
41
+ #
42
+ # Two bytes are for the [ and ] of JSON array syntax.
43
+ MAX_CHUNK_SIZE = 5 * 1024 * 1024 - 2
30
44
 
31
- @client = HTTP::Client.new(current_api, logger: logger)
32
- end
33
-
34
- def current_api
35
- @apis[HTTP::API::INPUT]
36
- end
45
+ # Try to send smaller payloads to avoid large network requests.
46
+ # If a payload is larger than default chunk size but is under the
47
+ # max chunk size, it will still get sent out.
48
+ DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024
37
49
 
38
50
  def send_input(payload, tags)
39
- json = JSON.dump(payload)
40
- parcel = EncodedParcel.new(json)
51
+ # Tags are the same for all chunks, serialize them one time.
41
52
  serialized_tags = Core::TagBuilder.serialize_tags(tags)
53
+
54
+ encoder = Core::Encoding::JSONEncoder
55
+ encoded_snapshots = Core::Utils::Array.filter_map(payload) do |snapshot|
56
+ encoded = encoder.encode(snapshot)
57
+ if encoded.length > MAX_SERIALIZED_SNAPSHOT_SIZE
58
+ # Drop the snapshot.
59
+ # TODO report via telemetry metric?
60
+ logger.debug { "di: dropping too big snapshot" }
61
+ nil
62
+ else
63
+ encoded
64
+ end
65
+ end
66
+
67
+ Datadog::Core::Chunker.chunk_by_size(
68
+ encoded_snapshots, DEFAULT_CHUNK_SIZE,
69
+ ).each do |chunk|
70
+ # We drop snapshots that are too big earlier.
71
+ # The limit on chunked payload length here is greater
72
+ # than the limit on snapshot size, therefore no chunks
73
+ # can exceed limits here.
74
+ chunked_payload = encoder.join(chunk)
75
+
76
+ # We need to rescue exceptions for each chunk so that
77
+ # subsequent chunks are attempted to be sent.
78
+ begin
79
+ send_input_chunk(chunked_payload, serialized_tags)
80
+ rescue => exc
81
+ logger.debug { "di: failed to send snapshot chunk: #{exc.class}: #{exc} (at #{exc.backtrace.first})" }
82
+ end
83
+ end
84
+
85
+ payload
86
+ end
87
+
88
+ def send_input_chunk(chunked_payload, serialized_tags)
89
+ parcel = EncodedParcel.new(chunked_payload)
42
90
  request = Request.new(parcel, serialized_tags)
43
91
 
44
- response = @client.send_input_payload(request)
45
- unless response.ok?
46
- # TODO Datadog::Core::Transport::InternalErrorResponse
47
- # does not have +code+ method, what is the actual API of
48
- # these response objects?
49
- raise Error::AgentCommunicationError, "send_input failed: #{begin
50
- response.code
51
- rescue
52
- "???"
53
- end}: #{response.payload}"
92
+ client.send_request(:input, request).tap do |response|
93
+ if downgrade?(response)
94
+ downgrade!
95
+ return send_input_chunk(chunked_payload, serialized_tags)
96
+ end
54
97
  end
55
- rescue Error::AgentCommunicationError
56
- raise
57
- # Datadog::Core::Transport does not perform any exception mapping,
58
- # therefore we could have any exception here from failure to parse
59
- # agent URI for example.
60
- # If we ever implement retries for network errors, we should distinguish
61
- # actual network errors from non-network errors that are raised by
62
- # transport code.
63
- rescue => exc
64
- raise Error::AgentCommunicationError, "send_input failed: #{exc.class}: #{exc}"
65
98
  end
66
99
  end
67
100
  end
data/lib/datadog/di.rb CHANGED
@@ -9,16 +9,13 @@ module Datadog
9
9
  #
10
10
  # @api private
11
11
  module DI
12
+ INSTRUMENTED_COUNTERS_LOCK = Mutex.new
13
+
12
14
  class << self
13
15
  def enabled?
14
16
  Datadog.configuration.dynamic_instrumentation.enabled
15
17
  end
16
- end
17
-
18
- # Expose DI to global shared objects
19
- Extensions.activate!
20
18
 
21
- class << self
22
19
  # This method is called from DI Remote handler to issue DI operations
23
20
  # to the probe manager (add or remove probes).
24
21
  #
@@ -31,6 +28,65 @@ module Datadog
31
28
  def component
32
29
  Datadog.send(:components).dynamic_instrumentation
33
30
  end
31
+
32
+ # Track how many outstanding instrumentations are in DI.
33
+ #
34
+ # It is hard to find the actual instrumentations - there is no
35
+ # method provided by Ruby to list all trace points, and we would
36
+ # need to manually track our instrumentation modules for method probes.
37
+ # Plus, tracking the modules could create active references to
38
+ # instrumentation, which is not desired.
39
+ #
40
+ # A simpler solution is to maintain a counter which is increased
41
+ # whenever a probe is installed and decreased when a probe is removed.
42
+ #
43
+ # This counter does not include pending probes - being not installed,
44
+ # those pose no concerns to customer applications.
45
+ def instrumented_count(kind = nil)
46
+ INSTRUMENTED_COUNTERS_LOCK.synchronize do
47
+ if defined?(@instrumented_count)
48
+ if kind
49
+ validate_kind!(kind)
50
+ @instrumented_count[kind] || 0
51
+ else
52
+ @instrumented_count.inject(0) do |sum, (_kind, count)|
53
+ sum + count
54
+ end
55
+ end
56
+ else
57
+ 0
58
+ end
59
+ end
60
+ end
61
+
62
+ def instrumented_count_inc(kind)
63
+ validate_kind!(kind)
64
+ INSTRUMENTED_COUNTERS_LOCK.synchronize do
65
+ @instrumented_count = Hash.new(0) unless defined?(@instrumented_count)
66
+ @instrumented_count[kind] += 1
67
+ end
68
+ end
69
+
70
+ def instrumented_count_dec(kind)
71
+ validate_kind!(kind)
72
+ INSTRUMENTED_COUNTERS_LOCK.synchronize do
73
+ @instrumented_count = Hash.new(0) unless defined?(@instrumented_count)
74
+ if @instrumented_count[kind] <= 0
75
+ Datadog.logger.debug { "di: attempting to decrease instrumented count below zero for #{kind}" }
76
+ return
77
+ end
78
+ @instrumented_count[kind] -= 1
79
+ end
80
+ end
81
+
82
+ private def validate_kind!(kind)
83
+ unless %i[line method].include?(kind)
84
+ raise ArgumentError, "Invalid kind: #{kind}"
85
+ end
86
+ end
34
87
  end
88
+
89
+ # Expose DI to global shared objects
90
+ Extensions.activate!
35
91
  end
36
92
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'transport'
4
+ require_relative 'evaluation_engine'
5
+ require_relative 'exposures/buffer'
6
+ require_relative 'exposures/worker'
7
+ require_relative 'exposures/deduplicator'
8
+ require_relative 'exposures/reporter'
9
+
10
+ module Datadog
11
+ module OpenFeature
12
+ # This class is the entry point for the OpenFeature component
13
+ class Component
14
+ attr_reader :engine
15
+
16
+ def self.build(settings, agent_settings, logger:, telemetry:)
17
+ return unless settings.respond_to?(:open_feature) && settings.open_feature.enabled
18
+
19
+ unless settings.respond_to?(:remote) && settings.remote.enabled
20
+ message = 'OpenFeature could not be enabled as Remote Configuration is currently disabled. ' \
21
+ 'To enable Remote Configuration, see https://docs.datadoghq.com/remote_configuration/.'
22
+
23
+ logger.warn(message)
24
+ return
25
+ end
26
+
27
+ if RUBY_ENGINE != 'ruby'
28
+ message = 'OpenFeature could not be enabled as MRI is required, ' \
29
+ "but running on #{RUBY_ENGINE.inspect}"
30
+
31
+ logger.warn(message)
32
+ return
33
+ end
34
+
35
+ if (libdatadog_api_failure = Core::LIBDATADOG_API_FAILURE)
36
+ message = 'OpenFeature could not be enabled as `libdatadog` is not loaded: ' \
37
+ "#{libdatadog_api_failure.inspect}. For help solving this issue, " \
38
+ 'please contact Datadog support at https://docs.datadoghq.com/help/.'
39
+
40
+ logger.warn(message)
41
+ return
42
+ end
43
+
44
+ new(settings, agent_settings, logger: logger, telemetry: telemetry)
45
+ end
46
+
47
+ def initialize(settings, agent_settings, logger:, telemetry:)
48
+ transport = Transport::HTTP.build(agent_settings: agent_settings, logger: logger)
49
+ @worker = Exposures::Worker.new(settings: settings, transport: transport, telemetry: telemetry, logger: logger)
50
+
51
+ reporter = Exposures::Reporter.new(@worker, telemetry: telemetry, logger: logger)
52
+ @engine = EvaluationEngine.new(reporter, telemetry: telemetry, logger: logger)
53
+ end
54
+
55
+ def shutdown!
56
+ @worker.graceful_shutdown
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Configuration
6
+ # A settings class for the OpenFeature component.
7
+ module Settings
8
+ def self.extended(base)
9
+ base = base.singleton_class unless base.is_a?(Class)
10
+ add_settings!(base)
11
+ end
12
+
13
+ def self.add_settings!(base)
14
+ base.class_eval do
15
+ settings :open_feature do
16
+ option :enabled do |o|
17
+ o.type :bool
18
+ o.env 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'
19
+ o.default false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ext'
4
+ require_relative 'noop_evaluator'
5
+ require_relative 'native_evaluator'
6
+ require_relative 'resolution_details'
7
+
8
+ module Datadog
9
+ module OpenFeature
10
+ # This class performs the evaluation of the feature flag
11
+ class EvaluationEngine
12
+ # Steep: https://github.com/soutaro/steep/issues/1880
13
+ ReconfigurationError = Class.new(StandardError) # steep:ignore IncompatibleAssignment
14
+
15
+ ALLOWED_TYPES = %i[boolean string number float integer object].freeze
16
+
17
+ def initialize(reporter, telemetry:, logger:)
18
+ @reporter = reporter
19
+ @telemetry = telemetry
20
+ @logger = logger
21
+
22
+ @evaluator = NoopEvaluator.new(nil)
23
+ end
24
+
25
+ def fetch_value(flag_key, default_value:, expected_type:, evaluation_context: nil)
26
+ unless ALLOWED_TYPES.include?(expected_type)
27
+ message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
28
+ return ResolutionDetails.build_error(
29
+ value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
30
+ )
31
+ end
32
+
33
+ context = evaluation_context&.fields.to_h
34
+ result = @evaluator.get_assignment(
35
+ flag_key, default_value: default_value, context: context, expected_type: expected_type
36
+ )
37
+
38
+ @reporter.report(result, flag_key: flag_key, context: evaluation_context)
39
+
40
+ result
41
+ rescue => e
42
+ @telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
43
+
44
+ ResolutionDetails.build_error(
45
+ value: default_value, error_code: Ext::GENERAL, error_message: e.message
46
+ )
47
+ end
48
+
49
+ # NOTE: In a currect implementation configuration is expected to be a raw
50
+ # JSON string containing feature flags (straight from the remote config)
51
+ # in the format expected by `libdatadog` without any modifications
52
+ def reconfigure!(configuration)
53
+ if configuration.nil?
54
+ @logger.debug('OpenFeature: Removing configuration')
55
+
56
+ return @evaluator = NoopEvaluator.new(configuration)
57
+ end
58
+
59
+ @evaluator = NativeEvaluator.new(configuration)
60
+ rescue => e
61
+ message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
62
+
63
+ @logger.error("#{message}, #{e.class}: #{e.message}")
64
+ @telemetry.report(e, description: "#{message} (#{e.class})")
65
+
66
+ raise ReconfigurationError, e.message
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Exposures
6
+ # This class builds a batch of exposures and context to be sent to the Agent
7
+ class BatchBuilder
8
+ def initialize(settings)
9
+ @context = build_context(settings)
10
+ end
11
+
12
+ def payload_for(events)
13
+ {
14
+ context: @context,
15
+ exposures: events
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ def build_context(settings)
22
+ context = {}
23
+ context[:env] = settings.env if settings.env
24
+ context[:service] = settings.service if settings.service
25
+ context[:version] = settings.version if settings.version
26
+
27
+ context
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/buffer/cruby'
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Exposures
8
+ # This class is a buffer for exposure events that evicts at random and
9
+ # keeps track of the number of dropped events
10
+ #
11
+ # WARNING: This class does not work as intended on JRuby
12
+ class Buffer < Core::Buffer::CRuby
13
+ DEFAULT_LIMIT = 1_000
14
+
15
+ attr_reader :dropped_count
16
+
17
+ def initialize(limit = DEFAULT_LIMIT)
18
+ @dropped = 0
19
+ @dropped_count = 0
20
+
21
+ super
22
+ end
23
+
24
+ protected
25
+
26
+ def drain!
27
+ drained = super
28
+
29
+ @dropped_count = @dropped
30
+ @dropped = 0
31
+
32
+ drained
33
+ end
34
+
35
+ def replace!(item)
36
+ @dropped += 1
37
+
38
+ super
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/utils/lru_cache'
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Exposures
8
+ # This class is a deduplication buffer based on LRU cache for exposure events
9
+ class Deduplicator
10
+ DEFAULT_CACHE_LIMIT = 1_000
11
+
12
+ def initialize(limit: DEFAULT_CACHE_LIMIT)
13
+ @cache = Datadog::Core::Utils::LRUCache.new(limit)
14
+ @mutex = Mutex.new
15
+ end
16
+
17
+ def duplicate?(key, value)
18
+ @mutex.synchronize do
19
+ stored = @cache[key]
20
+ return true if stored == value
21
+
22
+ @cache[key] = value
23
+ end
24
+
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/utils/time'
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Exposures
8
+ # A data model for an exposure event
9
+ module Event
10
+ TARGETING_KEY_FIELD = 'targeting_key'
11
+ ALLOWED_FIELD_TYPES = [String, Integer, Float, TrueClass, FalseClass].freeze
12
+
13
+ class << self
14
+ def cache_key(result, flag_key:, context:)
15
+ "#{flag_key}:#{context.targeting_key}"
16
+ end
17
+
18
+ def cache_value(result, flag_key:, context:)
19
+ "#{result.allocation_key}:#{result.variant}"
20
+ end
21
+
22
+ def build(result, flag_key:, context:)
23
+ {
24
+ timestamp: current_timestamp_ms,
25
+ allocation: {
26
+ key: result.allocation_key
27
+ },
28
+ flag: {
29
+ key: flag_key
30
+ },
31
+ variant: {
32
+ key: result.variant
33
+ },
34
+ subject: {
35
+ id: context.targeting_key,
36
+ attributes: extract_attributes(context)
37
+ }
38
+ }.freeze
39
+ end
40
+
41
+ private
42
+
43
+ # NOTE: We take all filds of the context that does not support nesting
44
+ # and will ignore targeting key as it will be set as `subject.id`
45
+ def extract_attributes(context)
46
+ context.fields.select do |key, value|
47
+ next false if key == TARGETING_KEY_FIELD
48
+
49
+ ALLOWED_FIELD_TYPES.include?(value.class)
50
+ end
51
+ end
52
+
53
+ def current_timestamp_ms
54
+ (Datadog::Core::Utils::Time.now.to_f * 1000).to_i
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+ require_relative 'deduplicator'
5
+
6
+ module Datadog
7
+ module OpenFeature
8
+ module Exposures
9
+ # This class is responsible for reporting exposures to the Agent
10
+ class Reporter
11
+ def initialize(worker, telemetry:, logger:)
12
+ @worker = worker
13
+ @logger = logger
14
+ @telemetry = telemetry
15
+ @deduplicator = Deduplicator.new
16
+ end
17
+
18
+ # NOTE: Reporting expects evaluation context to be always present, but it
19
+ # might be missing depending on the customer way of using flags evaluation API.
20
+ # In addition to that the evaluation result must be marked for reporting.
21
+ def report(result, flag_key:, context:)
22
+ return false if context.nil?
23
+ return false unless result.log?
24
+
25
+ key = Event.cache_key(result, flag_key: flag_key, context: context)
26
+ value = Event.cache_value(result, flag_key: flag_key, context: context)
27
+ return false if @deduplicator.duplicate?(key, value)
28
+
29
+ event = Event.build(result, flag_key: flag_key, context: context)
30
+ @worker.enqueue(event)
31
+ rescue => e
32
+ @logger.debug { "OpenFeature: Failed to report resolution details: #{e.class}: #{e.message}" }
33
+ @telemetry.report(e, description: 'OpenFeature: Failed to report resolution details')
34
+
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end