datadog 2.39.0 → 2.47.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 (296) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +269 -44
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +256 -193
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
  7. data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
  8. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
  9. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
  10. data/ext/datadog_profiling_native_extension/collectors_stack.c +267 -171
  11. data/ext/datadog_profiling_native_extension/collectors_stack.h +17 -3
  12. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +307 -217
  13. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +10 -5
  14. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +5 -4
  15. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +19 -11
  16. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +7 -10
  17. data/ext/datadog_profiling_native_extension/extconf.rb +67 -99
  18. data/ext/datadog_profiling_native_extension/heap_recorder.c +353 -257
  19. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  20. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +3 -27
  21. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +853 -496
  22. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +42 -15
  23. data/ext/datadog_profiling_native_extension/profiling.c +30 -23
  24. data/ext/datadog_profiling_native_extension/ruby_helpers.c +4 -80
  25. data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -7
  26. data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
  27. data/ext/datadog_profiling_native_extension/stack_recorder.c +116 -89
  28. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -5
  29. data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
  30. data/ext/datadog_profiling_native_extension/time_helpers.h +15 -15
  31. data/ext/libdatadog_api/datadog_ruby_common.c +19 -11
  32. data/ext/libdatadog_api/datadog_ruby_common.h +7 -10
  33. data/ext/libdatadog_api/di.c +43 -3
  34. data/ext/libdatadog_api/extconf.rb +6 -1
  35. data/ext/libdatadog_api/feature_flags.c +8 -0
  36. data/ext/libdatadog_api/init.c +9 -1
  37. data/ext/libdatadog_api/otel_thread_context.c +274 -0
  38. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  39. data/ext/libdatadog_api/process_discovery.c +15 -1
  40. data/ext/libdatadog_api/trace_exporter.c +2078 -0
  41. data/ext/libdatadog_api/trace_exporter.h +5 -0
  42. data/ext/libdatadog_extconf_helpers.rb +14 -9
  43. data/lib/datadog/ai_guard/component.rb +17 -8
  44. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  45. data/lib/datadog/ai_guard/configuration.rb +13 -0
  46. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  47. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  48. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  49. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  50. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  51. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  52. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  53. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  54. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  55. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  56. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  57. data/lib/datadog/ai_guard/evaluation/request.rb +8 -64
  58. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  59. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  60. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  61. data/lib/datadog/ai_guard/evaluation.rb +36 -14
  62. data/lib/datadog/ai_guard/ext.rb +4 -1
  63. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +7 -5
  64. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  65. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  66. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  67. data/lib/datadog/ai_guard/redaction.rb +80 -0
  68. data/lib/datadog/ai_guard.rb +58 -57
  69. data/lib/datadog/appsec/actions_handler/serializable_backtrace.rb +21 -42
  70. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +1 -1
  71. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +1 -1
  72. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +1 -1
  73. data/lib/datadog/appsec/assets/blocked.html +1 -108
  74. data/lib/datadog/appsec/configuration.rb +11 -2
  75. data/lib/datadog/appsec/context.rb +1 -1
  76. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +1 -1
  77. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +2 -2
  78. data/lib/datadog/appsec/contrib/devise/configuration.rb +1 -1
  79. data/lib/datadog/appsec/contrib/devise/patcher.rb +1 -1
  80. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +2 -2
  81. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +2 -2
  82. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +1 -1
  83. data/lib/datadog/appsec/contrib/rack/ext.rb +2 -2
  84. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +2 -2
  85. data/lib/datadog/appsec/contrib/rack/patcher.rb +1 -1
  86. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +6 -3
  87. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +30 -3
  88. data/lib/datadog/appsec/contrib/rails/patches/render_to_body_patch.rb +8 -7
  89. data/lib/datadog/appsec/contrib/rails/request_middleware.rb +2 -1
  90. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +2 -2
  91. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +15 -3
  92. data/lib/datadog/appsec/contrib/sinatra/patches/json_patch.rb +8 -7
  93. data/lib/datadog/appsec/contrib/sinatra/request_middleware.rb +2 -1
  94. data/lib/datadog/appsec/processor/rule_loader.rb +12 -12
  95. data/lib/datadog/appsec/response.rb +1 -1
  96. data/lib/datadog/appsec/route_normalizer/route_pattern.rb +3 -3
  97. data/lib/datadog/appsec/security_engine/engine.rb +5 -4
  98. data/lib/datadog/core/configuration/components.rb +50 -19
  99. data/lib/datadog/core/configuration/components_state.rb +8 -1
  100. data/lib/datadog/core/configuration/option_definition.rb +1 -1
  101. data/lib/datadog/core/configuration/options.rb +1 -1
  102. data/lib/datadog/core/configuration/settings.rb +38 -43
  103. data/lib/datadog/core/configuration/supported_configurations.rb +18 -2
  104. data/lib/datadog/core/crashtracking/component.rb +5 -1
  105. data/lib/datadog/core/diagnostics/environment_logger.rb +36 -1
  106. data/lib/datadog/core/environment/gc.rb +1 -1
  107. data/lib/datadog/core/environment/vm_cache.rb +1 -1
  108. data/lib/datadog/core/environment/yjit.rb +2 -2
  109. data/lib/datadog/core/metrics/helpers.rb +1 -1
  110. data/lib/datadog/core/metrics/options.rb +2 -2
  111. data/lib/datadog/core/process_discovery.rb +6 -2
  112. data/lib/datadog/core/remote/client/capabilities.rb +32 -21
  113. data/lib/datadog/core/remote/client.rb +45 -4
  114. data/lib/datadog/core/remote/component.rb +30 -7
  115. data/lib/datadog/core/remote/configuration/repository.rb +2 -2
  116. data/lib/datadog/core/remote/dispatcher.rb +47 -6
  117. data/lib/datadog/core/remote/worker.rb +8 -0
  118. data/lib/datadog/core/telemetry/configuration_value.rb +38 -0
  119. data/lib/datadog/core/telemetry/event/app_client_configuration_change.rb +3 -1
  120. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +1 -1
  121. data/lib/datadog/core/telemetry/event/app_started.rb +5 -24
  122. data/lib/datadog/core/telemetry/event/generate_metrics.rb +1 -1
  123. data/lib/datadog/core/telemetry/event/log.rb +2 -2
  124. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  125. data/lib/datadog/core/telemetry/metric.rb +2 -2
  126. data/lib/datadog/core/telemetry/request.rb +2 -2
  127. data/lib/datadog/core/transport/http.rb +1 -1
  128. data/lib/datadog/core/transport/transport.rb +1 -1
  129. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +170 -18
  130. data/lib/datadog/core/utils/sequence.rb +3 -6
  131. data/lib/datadog/core/utils/time.rb +16 -54
  132. data/lib/datadog/core/workers/runtime_metrics.rb +1 -1
  133. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  134. data/lib/datadog/data_streams/processor.rb +45 -14
  135. data/lib/datadog/di/boot.rb +1 -0
  136. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  137. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  138. data/lib/datadog/di/component.rb +108 -3
  139. data/lib/datadog/di/configuration.rb +53 -2
  140. data/lib/datadog/di/context.rb +7 -2
  141. data/lib/datadog/di/contrib/active_record.rb +8 -2
  142. data/lib/datadog/di/el/compiler.rb +88 -12
  143. data/lib/datadog/di/el/evaluator.rb +82 -2
  144. data/lib/datadog/di/el/expression.rb +13 -2
  145. data/lib/datadog/di/evaluation_failure.rb +28 -0
  146. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  147. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  148. data/lib/datadog/di/instrumenter.rb +106 -15
  149. data/lib/datadog/di/logger.rb +32 -10
  150. data/lib/datadog/di/probe.rb +9 -1
  151. data/lib/datadog/di/probe_builder.rb +7 -6
  152. data/lib/datadog/di/probe_manager.rb +56 -8
  153. data/lib/datadog/di/probe_notification_builder.rb +31 -28
  154. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  155. data/lib/datadog/di/proc_responder.rb +1 -1
  156. data/lib/datadog/di/remote.rb +49 -5
  157. data/lib/datadog/di/serializer.rb +172 -55
  158. data/lib/datadog/di/snapshot_event.rb +38 -0
  159. data/lib/datadog/di/transport/http.rb +13 -3
  160. data/lib/datadog/di/transport/input.rb +32 -7
  161. data/lib/datadog/error_tracking/component.rb +1 -1
  162. data/lib/datadog/kit/appsec/events/v2.rb +2 -2
  163. data/lib/datadog/open_feature/activation.rb +268 -0
  164. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  165. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  166. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  167. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  168. data/lib/datadog/open_feature/component.rb +85 -13
  169. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  170. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  171. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  172. data/lib/datadog/open_feature/configuration.rb +124 -0
  173. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  174. data/lib/datadog/open_feature/exposures/batch_builder.rb +1 -1
  175. data/lib/datadog/open_feature/exposures/event.rb +15 -8
  176. data/lib/datadog/open_feature/ext.rb +19 -0
  177. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  178. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  179. data/lib/datadog/open_feature/hooks/evaluation_telemetry_opt_out_hook.rb +25 -0
  180. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  181. data/lib/datadog/open_feature/native_evaluator.rb +20 -6
  182. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  183. data/lib/datadog/open_feature/provider.rb +315 -10
  184. data/lib/datadog/open_feature/remote.rb +5 -5
  185. data/lib/datadog/open_feature/transport.rb +5 -0
  186. data/lib/datadog/open_feature.rb +31 -1
  187. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  188. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  189. data/lib/datadog/opentelemetry/trace.rb +3 -0
  190. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +10 -2
  191. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  192. data/lib/datadog/profiling/collectors/info.rb +40 -13
  193. data/lib/datadog/profiling/collectors/thread_context.rb +6 -17
  194. data/lib/datadog/profiling/component.rb +33 -39
  195. data/lib/datadog/profiling/exporter.rb +10 -10
  196. data/lib/datadog/profiling/tasks/exec.rb +1 -1
  197. data/lib/datadog/profiling.rb +4 -0
  198. data/lib/datadog/symbol_database/extractor.rb +89 -107
  199. data/lib/datadog/tracing/component.rb +44 -2
  200. data/lib/datadog/tracing/configuration/dynamic/option.rb +4 -0
  201. data/lib/datadog/tracing/configuration/dynamic.rb +5 -3
  202. data/lib/datadog/tracing/configuration/ext.rb +4 -0
  203. data/lib/datadog/tracing/configuration/settings.rb +36 -0
  204. data/lib/datadog/tracing/context.rb +24 -2
  205. data/lib/datadog/tracing/context_provider.rb +17 -5
  206. data/lib/datadog/tracing/contrib/action_cable/events.rb +1 -1
  207. data/lib/datadog/tracing/contrib/action_mailer/events.rb +1 -1
  208. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +2 -2
  209. data/lib/datadog/tracing/contrib/action_view/events.rb +1 -1
  210. data/lib/datadog/tracing/contrib/active_model_serializers/events.rb +1 -1
  211. data/lib/datadog/tracing/contrib/active_record/configuration/resolver.rb +1 -1
  212. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  213. data/lib/datadog/tracing/contrib/active_record/events.rb +1 -1
  214. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +2 -2
  215. data/lib/datadog/tracing/contrib/aws/ext.rb +1 -1
  216. data/lib/datadog/tracing/contrib/aws/services.rb +1 -1
  217. data/lib/datadog/tracing/contrib/elasticsearch/quantize.rb +1 -1
  218. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +10 -5
  219. data/lib/datadog/tracing/contrib/excon/middleware.rb +10 -3
  220. data/lib/datadog/tracing/contrib/ext.rb +3 -3
  221. data/lib/datadog/tracing/contrib/faraday/middleware.rb +10 -4
  222. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  223. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +1 -1
  224. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +1 -1
  225. data/lib/datadog/tracing/contrib/grpc/distributed/propagation.rb +1 -1
  226. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +2 -1
  227. data/lib/datadog/tracing/contrib/http/instrumentation.rb +16 -1
  228. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +8 -2
  229. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +10 -4
  230. data/lib/datadog/tracing/contrib/kafka/events/connection/request.rb +4 -0
  231. data/lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb +7 -2
  232. data/lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb +7 -2
  233. data/lib/datadog/tracing/contrib/kafka/events.rb +1 -1
  234. data/lib/datadog/tracing/contrib/karafka/distributed/propagation.rb +1 -1
  235. data/lib/datadog/tracing/contrib/mongodb/parsers.rb +1 -1
  236. data/lib/datadog/tracing/contrib/opensearch/ext.rb +1 -1
  237. data/lib/datadog/tracing/contrib/opensearch/quantize.rb +1 -1
  238. data/lib/datadog/tracing/contrib/patchable.rb +1 -1
  239. data/lib/datadog/tracing/contrib/patcher.rb +1 -1
  240. data/lib/datadog/tracing/contrib/que/tracer.rb +1 -1
  241. data/lib/datadog/tracing/contrib/racecar/events.rb +1 -1
  242. data/lib/datadog/tracing/contrib/rack/configuration/settings.rb +1 -1
  243. data/lib/datadog/tracing/contrib/rack/header_tagging.rb +1 -1
  244. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  245. data/lib/datadog/tracing/contrib/rack/trace_proxy_middleware.rb +1 -1
  246. data/lib/datadog/tracing/contrib/redis/configuration/resolver.rb +1 -1
  247. data/lib/datadog/tracing/contrib/redis/quantize.rb +36 -2
  248. data/lib/datadog/tracing/contrib/redis/trace_middleware.rb +9 -3
  249. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +10 -3
  250. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -5
  251. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  252. data/lib/datadog/tracing/contrib/sequel/utils.rb +50 -63
  253. data/lib/datadog/tracing/contrib/sidekiq/distributed/propagation.rb +1 -1
  254. data/lib/datadog/tracing/contrib/sinatra/configuration/settings.rb +1 -1
  255. data/lib/datadog/tracing/contrib/sneakers/tracer.rb +1 -1
  256. data/lib/datadog/tracing/contrib/utils/quantization/hash_formatter.rb +1 -1
  257. data/lib/datadog/tracing/contrib/utils/quantization/http.rb +108 -3
  258. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +1 -1
  259. data/lib/datadog/tracing/correlation.rb +2 -2
  260. data/lib/datadog/tracing/diagnostics/environment_logger.rb +3 -2
  261. data/lib/datadog/tracing/distributed/baggage.rb +1 -3
  262. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  263. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  264. data/lib/datadog/tracing/distributed/propagation_policy.rb +5 -0
  265. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  266. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  267. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  268. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  269. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  270. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  271. data/lib/datadog/tracing/metadata/tagging.rb +1 -1
  272. data/lib/datadog/tracing/otel_thread_context.rb +97 -0
  273. data/lib/datadog/tracing/remote.rb +236 -37
  274. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  275. data/lib/datadog/tracing/span.rb +1 -1
  276. data/lib/datadog/tracing/span_operation.rb +1 -1
  277. data/lib/datadog/tracing/sync_writer.rb +8 -3
  278. data/lib/datadog/tracing/trace_digest.rb +22 -4
  279. data/lib/datadog/tracing/trace_operation.rb +22 -10
  280. data/lib/datadog/tracing/tracer.rb +28 -7
  281. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  282. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  283. data/lib/datadog/tracing/transport/io/traces.rb +1 -1
  284. data/lib/datadog/tracing/transport/native/response.rb +71 -0
  285. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  286. data/lib/datadog/tracing/transport/native.rb +395 -0
  287. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  288. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  289. data/lib/datadog/tracing/transport/trace_formatter.rb +14 -4
  290. data/lib/datadog/tracing/transport/traces.rb +85 -38
  291. data/lib/datadog/tracing/writer.rb +17 -4
  292. data/lib/datadog/version.rb +1 -1
  293. metadata +51 -13
  294. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
  295. data/lib/datadog/appsec/utils/hash_coercion.rb +0 -23
  296. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "agentless/configuration_source"
4
+ require_relative "component"
5
+ require_relative "configuration/agentless_endpoint"
6
+ require_relative "configuration/source"
7
+ require_relative "remote"
8
+
9
+ module Datadog
10
+ module OpenFeature
11
+ # Owns eager Remote Configuration and lazy agentless delivery activation.
12
+ class Activation
13
+ attr_reader :component, :failure
14
+
15
+ def initialize(settings, agent_settings, remote, logger:, telemetry:)
16
+ @settings = settings
17
+ @agent_settings = agent_settings
18
+ @remote = remote
19
+ @logger = logger
20
+ @telemetry = telemetry
21
+ @component = nil
22
+ @configuration_source = nil
23
+ @delivery_source = nil
24
+ @remote_receivers = nil
25
+ @failure = nil
26
+ @providers = {}.compare_by_identity
27
+ @activated = false
28
+ @delivery_started = false
29
+ @shutdown = false
30
+ @mutex = Mutex.new
31
+ end
32
+
33
+ def activate(provider)
34
+ @mutex.synchronize do
35
+ return if @shutdown
36
+ return unless open_feature_available?
37
+
38
+ @providers[provider] = true
39
+ return @component if @activated && @delivery_started
40
+ return if @activated
41
+
42
+ resolution = Configuration::Source.resolve(@settings, logger: @logger)
43
+ activate_delivery(resolution)
44
+ end
45
+ end
46
+
47
+ def startup!
48
+ start!
49
+ OpenFeature.reattach(self)
50
+ rescue => e
51
+ # Feature Flags is optional and must never interrupt library startup.
52
+ report_failure(e, "Feature Flags delivery failed to start")
53
+ end
54
+
55
+ def start!
56
+ @mutex.synchronize do
57
+ return if @shutdown || @activated
58
+ return unless open_feature_available?
59
+
60
+ resolution = Configuration::Source.resolve(@settings, logger: @logger)
61
+ return unless resolution.enabled? && resolution.source == Configuration::Source::REMOTE_CONFIG
62
+
63
+ activate_delivery(resolution)
64
+ end
65
+ end
66
+
67
+ def activated?
68
+ @mutex.synchronize { @activated }
69
+ end
70
+
71
+ def providers
72
+ @mutex.synchronize { @providers.keys }
73
+ end
74
+
75
+ def after_fork
76
+ inherited_providers, component = @mutex.synchronize do
77
+ @shutdown ? [[], nil] : [@providers.keys, @component]
78
+ end
79
+ inherited_providers.each { |provider| provider.send(:after_fork, component) }
80
+
81
+ configuration_source = @mutex.synchronize do
82
+ if @shutdown || !@delivery_started
83
+ nil
84
+ else
85
+ (@delivery_source == Configuration::Source::REMOTE_CONFIG) ? @remote : @configuration_source
86
+ end
87
+ end
88
+
89
+ configuration_source&.start
90
+ nil
91
+ rescue => e
92
+ # Feature Flags is optional and must never interrupt other post-fork handlers.
93
+ report_failure(e, "Feature Flags delivery failed to restart after fork")
94
+ end
95
+
96
+ def deactivate(provider)
97
+ configuration_source, component = @mutex.synchronize do
98
+ return if @shutdown
99
+ return unless @providers.delete(provider)
100
+ return unless @providers.empty?
101
+ if @delivery_source == Configuration::Source::REMOTE_CONFIG && @delivery_started
102
+ # Remote Configuration is process-scoped and may already hold configuration needed by the next provider.
103
+ [nil, nil]
104
+ else
105
+ previous_source = @configuration_source
106
+ previous_component = @component
107
+ @component = nil
108
+ @configuration_source = nil
109
+ @delivery_source = nil
110
+ @activated = false
111
+ @delivery_started = false
112
+ @failure = nil
113
+ [previous_source, previous_component]
114
+ end
115
+ end
116
+
117
+ configuration_source&.stop
118
+ component&.shutdown!
119
+ end
120
+
121
+ def shutdown!
122
+ configuration_source, component = @mutex.synchronize do
123
+ return if @shutdown
124
+
125
+ @shutdown = true
126
+ [@configuration_source, @component]
127
+ end
128
+
129
+ configuration_source&.stop
130
+ configuration_received = @mutex.synchronize do
131
+ !@providers.empty? && !!component&.configuration_received?
132
+ end
133
+ configuration_changed(Component::CONFIGURATION_LOST) if configuration_received
134
+ component&.shutdown!
135
+ end
136
+
137
+ private
138
+
139
+ def report_failure(error, description)
140
+ @logger.error("#{description}: #{error.class}: #{error.message}")
141
+ @telemetry.report(error, description: description)
142
+ end
143
+
144
+ def open_feature_available?
145
+ @settings.respond_to?(:open_feature) && @settings.respond_to?(:feature_flags)
146
+ end
147
+
148
+ def activate_delivery(resolution)
149
+ @activated = true
150
+ unless resolution.enabled?
151
+ @failure = "Feature Flags are disabled"
152
+ return
153
+ end
154
+
155
+ component = Component.build(
156
+ @settings,
157
+ @agent_settings,
158
+ resolution: resolution,
159
+ on_configuration_change: ->(event) { configuration_changed(event) },
160
+ logger: @logger,
161
+ telemetry: @telemetry,
162
+ )
163
+ unless component
164
+ @failure = "Feature Flags are unavailable on this runtime"
165
+ return
166
+ end
167
+
168
+ @component = component
169
+ @delivery_source = resolution.source
170
+ @delivery_started = start_delivery(resolution.source)
171
+ return component if @delivery_started
172
+
173
+ component.shutdown!
174
+ @component = nil
175
+ @delivery_source = nil
176
+ nil
177
+ rescue
178
+ configuration_source = @configuration_source
179
+ failed_component = @component
180
+ @component = nil
181
+ @configuration_source = nil
182
+ @delivery_source = nil
183
+ @activated = false
184
+ @delivery_started = false
185
+ @failure = nil
186
+ begin
187
+ configuration_source&.stop
188
+ ensure
189
+ failed_component&.shutdown!
190
+ end
191
+ raise
192
+ end
193
+
194
+ def start_delivery(source)
195
+ case source
196
+ when Configuration::Source::AGENTLESS
197
+ start_agentless
198
+ when Configuration::Source::REMOTE_CONFIG
199
+ start_remote_configuration
200
+ else
201
+ @failure = "Feature Flags configuration source is unavailable"
202
+ false
203
+ end
204
+ end
205
+
206
+ def start_agentless
207
+ component = @component
208
+ return false unless component
209
+
210
+ endpoint = Configuration::AgentlessEndpoint.build(
211
+ site: @settings.site,
212
+ environment: @settings.env,
213
+ base_url: @settings.feature_flags.agentless.base_url,
214
+ logger: @logger,
215
+ )
216
+ unless endpoint
217
+ @failure = "Feature Flags agentless endpoint is unavailable"
218
+ return false
219
+ end
220
+
221
+ configuration_source = Agentless::ConfigurationSource.build(
222
+ @settings,
223
+ endpoint: endpoint,
224
+ apply: ->(configuration) { component.reconfigure!(configuration) },
225
+ logger: @logger,
226
+ )
227
+ unless configuration_source
228
+ @failure = "Feature Flags agentless delivery could not start"
229
+ return false
230
+ end
231
+
232
+ @configuration_source = configuration_source
233
+ started = configuration_source.start
234
+ @failure = "Feature Flags agentless delivery could not start" unless started
235
+ started
236
+ end
237
+
238
+ def start_remote_configuration
239
+ remote = @remote
240
+ unless remote
241
+ @failure = "Feature Flags Remote Configuration is unavailable"
242
+ @logger.warn(
243
+ "#{@failure}. To enable Remote Configuration, " \
244
+ "see https://docs.datadoghq.com/remote_configuration/."
245
+ )
246
+ return false
247
+ end
248
+
249
+ receivers = @remote_receivers ||= Remote.receivers(
250
+ @telemetry,
251
+ component_provider: -> { @mutex.synchronize { @component } },
252
+ )
253
+ remote.register(
254
+ capabilities: Remote.capabilities,
255
+ products: Remote.products,
256
+ receivers: receivers,
257
+ )
258
+ remote.start
259
+ true
260
+ end
261
+
262
+ def configuration_changed(event)
263
+ providers = @mutex.synchronize { @providers.keys }
264
+ providers.each { |provider| provider.send(:configuration_changed, event) }
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/workers/polling"
4
+ require_relative "../evaluation_engine"
5
+ require_relative "parser"
6
+ require_relative "transport"
7
+
8
+ module Datadog
9
+ module OpenFeature
10
+ module Agentless
11
+ # Polls the agentless endpoint and applies accepted configuration.
12
+ class ConfigurationSource
13
+ MAX_ATTEMPTS = 3
14
+ FIRST_RETRY_MIN_SECONDS = 2.0
15
+ FIRST_RETRY_MAX_SECONDS = 10.0
16
+ SECOND_RETRY_MIN_SECONDS = 5.0
17
+ SECOND_RETRY_MAX_SECONDS = 30.0
18
+ RETRY_JITTER = 0.2
19
+
20
+ include Core::Workers::Polling
21
+
22
+ attr_reader :etag
23
+
24
+ def self.build(settings, endpoint:, apply:, logger: Datadog.logger)
25
+ api_key = settings.api_key.to_s.strip
26
+ if endpoint.managed? && api_key.empty?
27
+ logger.warn("Feature Flags agentless delivery requires DD_API_KEY; agentless delivery is disabled")
28
+ return
29
+ end
30
+
31
+ new(
32
+ endpoint: endpoint,
33
+ api_key: api_key.empty? ? nil : api_key,
34
+ poll_interval_seconds: settings.feature_flags.agentless.poll_interval_seconds,
35
+ request_timeout_seconds: settings.feature_flags.agentless.request_timeout_seconds,
36
+ apply: apply,
37
+ logger: logger,
38
+ )
39
+ end
40
+
41
+ def initialize(
42
+ endpoint:,
43
+ api_key:,
44
+ poll_interval_seconds:,
45
+ request_timeout_seconds:,
46
+ apply:,
47
+ logger:,
48
+ transport: nil,
49
+ random: nil,
50
+ retry_wait: nil
51
+ )
52
+ @transport = transport || Transport.new(
53
+ endpoint: endpoint,
54
+ api_key: api_key,
55
+ timeout_seconds: request_timeout_seconds,
56
+ )
57
+ @apply = apply
58
+ @logger = logger
59
+ @poll_interval_seconds = poll_interval_seconds
60
+ @random = random || -> { Kernel.rand }
61
+ @retry_wait = retry_wait || ->(delay) { wait_for_retry(delay) }
62
+ @etag = nil
63
+ @warned = {}
64
+ @lifecycle_mutex = Mutex.new
65
+ @started = false
66
+ @stopped = false
67
+
68
+ self.enabled = true
69
+ self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
70
+ self.loop_base_interval = poll_interval_seconds
71
+ end
72
+
73
+ def start
74
+ @lifecycle_mutex.synchronize do
75
+ return false if @stopped
76
+ return true if @started && !forked?
77
+
78
+ perform
79
+ @started = true
80
+ end
81
+
82
+ true
83
+ end
84
+
85
+ def stop(force_stop = true, timeout = Core::Workers::Polling::DEFAULT_SHUTDOWN_TIMEOUT)
86
+ @lifecycle_mutex.synchronize do
87
+ return false if @stopped
88
+
89
+ @stopped = true
90
+ self.enabled = false
91
+ end
92
+
93
+ super
94
+ true
95
+ end
96
+
97
+ def perform
98
+ poll
99
+ # An exception escaping here ends the unsupervised worker thread and freezes configuration.
100
+ rescue => error
101
+ warn_once(:unexpected) do
102
+ @logger.error("Feature Flags agentless poll failed with #{error.class}; polling continues")
103
+ end
104
+ end
105
+
106
+ def poll
107
+ attempt = 1
108
+
109
+ while attempt <= MAX_ATTEMPTS
110
+ return if stopped?
111
+
112
+ response = @transport.get(@etag)
113
+ unless retryable?(response)
114
+ apply_response(response)
115
+ return
116
+ end
117
+
118
+ if attempt == MAX_ATTEMPTS
119
+ warn_failure(response, attempt)
120
+ return
121
+ end
122
+
123
+ return if wait_before_retry(retry_delay(attempt))
124
+
125
+ attempt += 1
126
+ end
127
+ end
128
+
129
+ protected
130
+
131
+ def after_fork
132
+ # The child must report its own failures, while the inherited ETag still matches its configuration.
133
+ @warned = {}
134
+ nil
135
+ end
136
+
137
+ private
138
+
139
+ def stopped?
140
+ @lifecycle_mutex.synchronize { @stopped }
141
+ end
142
+
143
+ def retryable?(response)
144
+ status = response.status
145
+ return true unless status
146
+
147
+ retryable_status?(status)
148
+ end
149
+
150
+ def retryable_status?(status)
151
+ status == 408 || status == 429 || status.between?(500, 599)
152
+ end
153
+
154
+ def retry_delay(attempt)
155
+ base = if attempt == 1
156
+ clamp(@poll_interval_seconds / 6.0, FIRST_RETRY_MIN_SECONDS, FIRST_RETRY_MAX_SECONDS)
157
+ else
158
+ clamp(@poll_interval_seconds / 3.0, SECOND_RETRY_MIN_SECONDS, SECOND_RETRY_MAX_SECONDS)
159
+ end
160
+ random = @random.call
161
+ jitter = 1 - RETRY_JITTER + (random * RETRY_JITTER * 2)
162
+ [1.0, base * jitter].max
163
+ end
164
+
165
+ def clamp(value, minimum, maximum)
166
+ value.clamp(minimum, maximum)
167
+ end
168
+
169
+ def wait_before_retry(delay)
170
+ @retry_wait.call(delay)
171
+ end
172
+
173
+ def wait_for_retry(delay)
174
+ mutex.synchronize do
175
+ return true if stopped?
176
+
177
+ shutdown.wait(mutex, delay)
178
+ end
179
+
180
+ stopped? || !run_loop?
181
+ end
182
+
183
+ def apply_response(response)
184
+ case response.status
185
+ when 200
186
+ apply_configuration(response)
187
+ when 304
188
+ nil
189
+ when 401, 403
190
+ warn_once(:authentication) do
191
+ @logger.warn(
192
+ "Feature Flags agentless endpoint returned HTTP #{response.status}; verify endpoint authentication"
193
+ )
194
+ end
195
+ else
196
+ warn_failure(response, 1)
197
+ end
198
+ end
199
+
200
+ def apply_configuration(response)
201
+ configuration = response.body && Parser.parse(response.body)
202
+ unless configuration
203
+ warn_once(:malformed) do
204
+ @logger.error("Feature Flags agentless endpoint returned malformed UFC payload")
205
+ end
206
+ return
207
+ end
208
+
209
+ @apply.call(configuration)
210
+ etag = response.etag.to_s.strip
211
+ @etag = etag.empty? ? nil : etag
212
+ rescue EvaluationEngine::ReconfigurationError => error
213
+ warn_once(:application) do
214
+ @logger.warn("Feature Flags agentless UFC payload could not be applied: #{error.class}: #{error.message}")
215
+ end
216
+ end
217
+
218
+ def warn_failure(response, attempts)
219
+ status = response.status
220
+ category = if status.nil?
221
+ :request
222
+ elsif retryable_status?(status)
223
+ :http_retryable
224
+ else
225
+ :http_unexpected
226
+ end
227
+ warn_once(category) do
228
+ if status
229
+ @logger.warn("Feature Flags agentless endpoint returned HTTP #{status} after #{attempts} attempt(s)")
230
+ else
231
+ error_class = response.error ? response.error.class : StandardError
232
+ @logger.warn("Feature Flags agentless request failed after #{attempts} attempt(s): #{error_class}")
233
+ end
234
+ end
235
+ end
236
+
237
+ def warn_once(category)
238
+ return if @warned[category]
239
+
240
+ @warned[category] = true
241
+ yield
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Agentless
8
+ # Parses the JSON:API envelope returned by agentless delivery.
9
+ module Parser
10
+ RESOURCE_TYPE = "universal-flag-configuration"
11
+
12
+ module_function
13
+
14
+ def parse(body)
15
+ payload = JSON.parse(body)
16
+ return unless payload.is_a?(Hash)
17
+
18
+ data = payload["data"]
19
+ return unless data.is_a?(Hash) && data["type"] == RESOURCE_TYPE
20
+
21
+ attributes = data["attributes"]
22
+ return unless valid_attributes?(attributes)
23
+
24
+ JSON.generate(attributes)
25
+ rescue JSON::ParserError, JSON::GeneratorError, TypeError
26
+ nil
27
+ end
28
+
29
+ def valid_attributes?(attributes)
30
+ return false unless attributes.is_a?(Hash)
31
+
32
+ environment = attributes["environment"]
33
+ attributes["format"].is_a?(String) &&
34
+ attributes["createdAt"].is_a?(String) &&
35
+ environment.is_a?(Hash) &&
36
+ environment["name"].is_a?(String) &&
37
+ attributes["flags"].is_a?(Hash)
38
+ end
39
+ private_class_method :valid_attributes?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Agentless
6
+ # Result of one agentless configuration request.
7
+ class Response
8
+ attr_reader :status, :etag, :body, :error
9
+
10
+ def initialize(status: nil, etag: nil, body: nil, error: nil)
11
+ @status = status
12
+ @etag = etag
13
+ @body = body
14
+ @error = error
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "stringio"
5
+ require "timeout"
6
+ require "uri"
7
+ require "zlib"
8
+
9
+ require_relative "../../core/environment/identity"
10
+ require_relative "../../core/transport/ext"
11
+ require_relative "response"
12
+
13
+ module Datadog
14
+ module OpenFeature
15
+ module Agentless
16
+ # HTTP transport for agentless configuration delivery.
17
+ class Transport
18
+ def initialize(endpoint:, api_key:, timeout_seconds:)
19
+ @endpoint = endpoint
20
+ @api_key = endpoint.managed? ? api_key : nil
21
+ @timeout_seconds = timeout_seconds
22
+ end
23
+
24
+ def get(etag)
25
+ request = Net::HTTP::Get.new(@endpoint.uri.request_uri, headers(etag))
26
+ apply_custom_authentication(request)
27
+ response = request(request)
28
+ status = response.code.to_i
29
+
30
+ Response.new(
31
+ status: status,
32
+ etag: response["ETag"],
33
+ # Decoding another status can erase the status the poller needs to classify the response.
34
+ body: (status == 200) ? response_body(response) : nil,
35
+ )
36
+ # Net::HTTP uses several version-specific exception types. None may end
37
+ # the delivery worker; the poller classifies every transport failure.
38
+ rescue => error
39
+ Response.new(error: error)
40
+ end
41
+
42
+ private
43
+
44
+ def apply_custom_authentication(request)
45
+ return if @endpoint.managed?
46
+
47
+ uri = @endpoint.uri
48
+ username = uri.user
49
+ return unless username
50
+
51
+ request.basic_auth(
52
+ URI::RFC2396_PARSER.unescape(username),
53
+ URI::RFC2396_PARSER.unescape(uri.password.to_s),
54
+ )
55
+ end
56
+
57
+ def headers(etag)
58
+ headers = {
59
+ "Accept-Encoding" => "gzip",
60
+ "DD-Client-Library-Language" => Core::Environment::Identity.lang,
61
+ "DD-Client-Library-Version" => Core::Environment::Identity.gem_datadog_version_semver2,
62
+ Core::Transport::Ext::HTTP::HEADER_DD_INTERNAL_UNTRACED_REQUEST => "1",
63
+ }
64
+ api_key = @api_key
65
+ headers[Core::Transport::Ext::HTTP::HEADER_DD_API_KEY] = api_key if api_key
66
+ headers["If-None-Match"] = etag if etag
67
+ headers
68
+ end
69
+
70
+ def request(request)
71
+ uri = @endpoint.uri
72
+ hostname = uri.hostname
73
+ raise ArgumentError, "Feature Flags agentless endpoint must have a host" unless hostname
74
+
75
+ # Agentless delivery requires public egress, so use Ruby's standard proxy discovery.
76
+ http = Net::HTTP.new(hostname, uri.port)
77
+ http.max_retries = 0
78
+ http.use_ssl = uri.scheme == "https"
79
+ http.open_timeout = @timeout_seconds
80
+ http.read_timeout = @timeout_seconds
81
+ http.write_timeout = @timeout_seconds if http.respond_to?(:write_timeout=)
82
+
83
+ # Bounds total request time, including DNS resolution, which the per-phase timeouts do not cover.
84
+ Timeout.timeout(@timeout_seconds) do
85
+ http.start { |connection| connection.request(request) }
86
+ end
87
+ end
88
+
89
+ def response_body(response)
90
+ body = response.body.to_s
91
+ return body unless response["Content-Encoding"].to_s.strip.casecmp("gzip") == 0
92
+
93
+ reader = Zlib::GzipReader.new(StringIO.new(body))
94
+ begin
95
+ reader.read.to_s
96
+ ensure
97
+ reader.close
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end