datadog 2.21.0 → 2.23.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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -2
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +3 -0
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +4 -0
  6. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +1 -1
  7. data/ext/datadog_profiling_native_extension/extconf.rb +6 -4
  8. data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
  9. data/ext/libdatadog_api/datadog_ruby_common.h +1 -1
  10. data/ext/libdatadog_api/ddsketch.c +106 -0
  11. data/ext/libdatadog_api/feature_flags.c +554 -0
  12. data/ext/libdatadog_api/feature_flags.h +5 -0
  13. data/ext/libdatadog_api/init.c +5 -0
  14. data/ext/libdatadog_api/library_config.c +34 -25
  15. data/ext/libdatadog_api/process_discovery.c +19 -13
  16. data/ext/libdatadog_extconf_helpers.rb +1 -1
  17. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  18. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  19. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  20. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  21. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  22. data/lib/datadog/appsec/api_security/route_extractor.rb +23 -6
  23. data/lib/datadog/appsec/api_security/sampler.rb +7 -4
  24. data/lib/datadog/appsec/assets/blocked.html +8 -0
  25. data/lib/datadog/appsec/assets/blocked.json +1 -1
  26. data/lib/datadog/appsec/assets/blocked.text +3 -1
  27. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  28. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  29. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  30. data/lib/datadog/appsec/assets.rb +1 -1
  31. data/lib/datadog/appsec/compressed_json.rb +1 -1
  32. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  33. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  34. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  35. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  36. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  38. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  39. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  40. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  41. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  42. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  43. data/lib/datadog/appsec/event.rb +12 -14
  44. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  45. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  46. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  47. data/lib/datadog/appsec/remote.rb +29 -13
  48. data/lib/datadog/appsec/response.rb +18 -4
  49. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  50. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  51. data/lib/datadog/appsec/security_event.rb +5 -7
  52. data/lib/datadog/core/configuration/components.rb +44 -9
  53. data/lib/datadog/core/configuration/config_helper.rb +1 -1
  54. data/lib/datadog/core/configuration/settings.rb +14 -0
  55. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  56. data/lib/datadog/core/configuration/supported_configurations.rb +330 -299
  57. data/lib/datadog/core/configuration.rb +1 -1
  58. data/lib/datadog/core/ddsketch.rb +19 -0
  59. data/lib/datadog/core/environment/ext.rb +6 -0
  60. data/lib/datadog/core/environment/process.rb +79 -0
  61. data/lib/datadog/core/environment/yjit.rb +2 -1
  62. data/lib/datadog/core/feature_flags.rb +61 -0
  63. data/lib/datadog/core/pin.rb +4 -8
  64. data/lib/datadog/core/process_discovery.rb +4 -2
  65. data/lib/datadog/core/remote/client/capabilities.rb +7 -0
  66. data/lib/datadog/core/remote/component.rb +4 -6
  67. data/lib/datadog/core/remote/transport/config.rb +2 -10
  68. data/lib/datadog/core/remote/transport/http/config.rb +9 -9
  69. data/lib/datadog/core/remote/transport/http/negotiation.rb +17 -8
  70. data/lib/datadog/core/remote/transport/http.rb +2 -0
  71. data/lib/datadog/core/remote/transport/negotiation.rb +2 -18
  72. data/lib/datadog/core/remote/worker.rb +25 -37
  73. data/lib/datadog/core/tag_builder.rb +0 -4
  74. data/lib/datadog/core/tag_normalizer.rb +84 -0
  75. data/lib/datadog/core/telemetry/component.rb +18 -3
  76. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  77. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  78. data/lib/datadog/core/telemetry/event/app_started.rb +52 -49
  79. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  80. data/lib/datadog/core/telemetry/event.rb +1 -0
  81. data/lib/datadog/core/telemetry/logger.rb +2 -2
  82. data/lib/datadog/core/telemetry/logging.rb +2 -8
  83. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +5 -6
  84. data/lib/datadog/core/telemetry/transport/telemetry.rb +1 -2
  85. data/lib/datadog/core/transport/http/client.rb +69 -0
  86. data/lib/datadog/core/transport/response.rb +4 -1
  87. data/lib/datadog/core/utils/array.rb +29 -0
  88. data/lib/datadog/{appsec/api_security → core/utils}/lru_cache.rb +10 -21
  89. data/lib/datadog/core/utils/network.rb +22 -1
  90. data/lib/datadog/core/utils/only_once_successful.rb +6 -2
  91. data/lib/datadog/core/utils.rb +2 -0
  92. data/lib/datadog/data_streams/configuration/settings.rb +49 -0
  93. data/lib/datadog/data_streams/configuration.rb +11 -0
  94. data/lib/datadog/data_streams/ext.rb +11 -0
  95. data/lib/datadog/data_streams/extensions.rb +16 -0
  96. data/lib/datadog/data_streams/pathway_context.rb +169 -0
  97. data/lib/datadog/data_streams/processor.rb +509 -0
  98. data/lib/datadog/data_streams/transport/http/api.rb +33 -0
  99. data/lib/datadog/data_streams/transport/http/client.rb +21 -0
  100. data/lib/datadog/data_streams/transport/http/stats.rb +87 -0
  101. data/lib/datadog/data_streams/transport/http.rb +41 -0
  102. data/lib/datadog/data_streams/transport/stats.rb +60 -0
  103. data/lib/datadog/data_streams.rb +100 -0
  104. data/lib/datadog/di/boot.rb +1 -0
  105. data/lib/datadog/di/component.rb +14 -16
  106. data/lib/datadog/di/context.rb +70 -0
  107. data/lib/datadog/di/el/compiler.rb +164 -0
  108. data/lib/datadog/di/el/evaluator.rb +159 -0
  109. data/lib/datadog/di/el/expression.rb +42 -0
  110. data/lib/datadog/di/el.rb +5 -0
  111. data/lib/datadog/di/error.rb +29 -0
  112. data/lib/datadog/di/instrumenter.rb +163 -48
  113. data/lib/datadog/di/probe.rb +55 -15
  114. data/lib/datadog/di/probe_builder.rb +39 -1
  115. data/lib/datadog/di/probe_manager.rb +13 -4
  116. data/lib/datadog/di/probe_notification_builder.rb +105 -67
  117. data/lib/datadog/di/proc_responder.rb +32 -0
  118. data/lib/datadog/di/serializer.rb +151 -7
  119. data/lib/datadog/di/transport/diagnostics.rb +2 -2
  120. data/lib/datadog/di/transport/http/diagnostics.rb +2 -4
  121. data/lib/datadog/di/transport/http/input.rb +2 -4
  122. data/lib/datadog/di/transport/http.rb +6 -2
  123. data/lib/datadog/di/transport/input.rb +64 -4
  124. data/lib/datadog/open_feature/component.rb +60 -0
  125. data/lib/datadog/open_feature/configuration.rb +27 -0
  126. data/lib/datadog/open_feature/evaluation_engine.rb +69 -0
  127. data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
  128. data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
  129. data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
  130. data/lib/datadog/open_feature/exposures/event.rb +60 -0
  131. data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
  132. data/lib/datadog/open_feature/exposures/worker.rb +116 -0
  133. data/lib/datadog/open_feature/ext.rb +14 -0
  134. data/lib/datadog/open_feature/native_evaluator.rb +38 -0
  135. data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
  136. data/lib/datadog/open_feature/provider.rb +141 -0
  137. data/lib/datadog/open_feature/remote.rb +74 -0
  138. data/lib/datadog/open_feature/resolution_details.rb +35 -0
  139. data/lib/datadog/open_feature/transport.rb +72 -0
  140. data/lib/datadog/open_feature.rb +19 -0
  141. data/lib/datadog/opentelemetry/configuration/settings.rb +159 -0
  142. data/lib/datadog/opentelemetry/metrics.rb +110 -0
  143. data/lib/datadog/opentelemetry/sdk/configurator.rb +25 -1
  144. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +38 -0
  145. data/lib/datadog/opentelemetry.rb +3 -0
  146. data/lib/datadog/profiling/collectors/code_provenance.rb +15 -6
  147. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  148. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -1
  149. data/lib/datadog/profiling/profiler.rb +4 -0
  150. data/lib/datadog/profiling/tag_builder.rb +36 -3
  151. data/lib/datadog/profiling.rb +1 -2
  152. data/lib/datadog/single_step_instrument.rb +1 -1
  153. data/lib/datadog/tracing/component.rb +6 -17
  154. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  155. data/lib/datadog/tracing/configuration/ext.rb +9 -0
  156. data/lib/datadog/tracing/configuration/settings.rb +77 -3
  157. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -4
  158. data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -2
  159. data/lib/datadog/tracing/contrib/active_job/log_injection.rb +21 -7
  160. data/lib/datadog/tracing/contrib/active_job/patcher.rb +5 -1
  161. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +4 -2
  162. data/lib/datadog/tracing/contrib/component.rb +2 -2
  163. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -1
  164. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +11 -3
  165. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +11 -7
  166. data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +7 -3
  167. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  168. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  169. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +74 -44
  170. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +11 -3
  171. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +11 -3
  172. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +11 -3
  173. data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +66 -0
  174. data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +66 -0
  175. data/lib/datadog/tracing/contrib/kafka/patcher.rb +14 -0
  176. data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
  177. data/lib/datadog/tracing/contrib/karafka/monitor.rb +11 -0
  178. data/lib/datadog/tracing/contrib/karafka/patcher.rb +32 -0
  179. data/lib/datadog/tracing/contrib/rack/middlewares.rb +59 -27
  180. data/lib/datadog/tracing/contrib/rack/route_inference.rb +53 -0
  181. data/lib/datadog/tracing/contrib/rails/middlewares.rb +2 -2
  182. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +4 -1
  183. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +3 -1
  184. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +3 -1
  185. data/lib/datadog/tracing/contrib/status_range_matcher.rb +7 -0
  186. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
  187. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
  188. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
  189. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
  190. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
  191. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +46 -0
  192. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
  193. data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
  194. data/lib/datadog/tracing/contrib.rb +1 -0
  195. data/lib/datadog/tracing/metadata/ext.rb +9 -1
  196. data/lib/datadog/tracing/transport/http/client.rb +12 -26
  197. data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
  198. data/lib/datadog/tracing/transport/traces.rb +3 -5
  199. data/lib/datadog/version.rb +2 -2
  200. data/lib/datadog.rb +2 -0
  201. metadata +92 -16
  202. data/ext/libdatadog_api/macos_development.md +0 -26
  203. data/lib/datadog/core/remote/transport/http/client.rb +0 -49
  204. data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
  205. data/lib/datadog/di/transport/http/client.rb +0 -47
@@ -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,69 @@
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
+ ReconfigurationError = Class.new(StandardError)
13
+
14
+ ALLOWED_TYPES = %i[boolean string number float integer object].freeze
15
+
16
+ def initialize(reporter, telemetry:, logger:)
17
+ @reporter = reporter
18
+ @telemetry = telemetry
19
+ @logger = logger
20
+
21
+ @evaluator = NoopEvaluator.new(nil)
22
+ end
23
+
24
+ def fetch_value(flag_key, default_value:, expected_type:, evaluation_context: nil)
25
+ unless ALLOWED_TYPES.include?(expected_type)
26
+ message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
27
+ return ResolutionDetails.build_error(
28
+ value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
29
+ )
30
+ end
31
+
32
+ context = evaluation_context&.fields.to_h
33
+ result = @evaluator.get_assignment(
34
+ flag_key, default_value: default_value, context: context, expected_type: expected_type
35
+ )
36
+
37
+ @reporter.report(result, flag_key: flag_key, context: evaluation_context)
38
+
39
+ result
40
+ rescue => e
41
+ @telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
42
+
43
+ ResolutionDetails.build_error(
44
+ value: default_value, error_code: Ext::GENERAL, error_message: e.message
45
+ )
46
+ end
47
+
48
+ # NOTE: In a currect implementation configuration is expected to be a raw
49
+ # JSON string containing feature flags (straight from the remote config)
50
+ # in the format expected by `libdatadog` without any modifications
51
+ def reconfigure!(configuration)
52
+ if configuration.nil?
53
+ @logger.debug('OpenFeature: Removing configuration')
54
+
55
+ return @evaluator = NoopEvaluator.new(configuration)
56
+ end
57
+
58
+ @evaluator = NativeEvaluator.new(configuration)
59
+ rescue => e
60
+ message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
61
+
62
+ @logger.error("#{message}, #{e.class}: #{e.message}")
63
+ @telemetry.report(e, description: "#{message} (#{e.class})")
64
+
65
+ raise ReconfigurationError, e.message
66
+ end
67
+ end
68
+ end
69
+ 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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../core/utils/time'
4
+ require_relative '../../core/workers/queue'
5
+ require_relative '../../core/workers/polling'
6
+
7
+ require_relative 'buffer'
8
+ require_relative 'batch_builder'
9
+
10
+ module Datadog
11
+ module OpenFeature
12
+ module Exposures
13
+ # This class is responsible for sending exposures to the Agent
14
+ class Worker
15
+ include Core::Workers::Queue
16
+ include Core::Workers::Polling
17
+
18
+ GRACEFUL_SHUTDOWN_EXTRA_SECONDS = 5
19
+ GRACEFUL_SHUTDOWN_WAIT_INTERVAL_SECONDS = 0.5
20
+
21
+ DEFAULT_FLUSH_INTERVAL_SECONDS = 30
22
+ DEFAULT_BUFFER_LIMIT = Buffer::DEFAULT_LIMIT
23
+
24
+ def initialize(
25
+ settings:,
26
+ transport:,
27
+ telemetry:,
28
+ logger:,
29
+ flush_interval_seconds: DEFAULT_FLUSH_INTERVAL_SECONDS,
30
+ buffer_limit: DEFAULT_BUFFER_LIMIT
31
+ )
32
+ @logger = logger
33
+ @transport = transport
34
+ @telemetry = telemetry
35
+ @batch_builder = BatchBuilder.new(settings)
36
+ @buffer_limit = buffer_limit
37
+
38
+ self.buffer = Buffer.new(buffer_limit)
39
+ self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
40
+ self.loop_base_interval = flush_interval_seconds
41
+ self.enabled = true
42
+ end
43
+
44
+ def start
45
+ return if !enabled? || running?
46
+
47
+ perform
48
+ end
49
+
50
+ def stop(force_stop = false, timeout = Core::Workers::Polling::DEFAULT_SHUTDOWN_TIMEOUT)
51
+ buffer.close if running?
52
+
53
+ super
54
+ end
55
+
56
+ def enqueue(event)
57
+ buffer.push(event)
58
+ start unless running?
59
+
60
+ true
61
+ end
62
+
63
+ def dequeue
64
+ [buffer.pop, buffer.dropped_count]
65
+ end
66
+
67
+ def perform(*args)
68
+ events, dropped = args
69
+ send_events(Array(events), dropped.to_i)
70
+ end
71
+
72
+ def graceful_shutdown
73
+ return false unless enabled? || !run_loop?
74
+
75
+ self.enabled = false
76
+
77
+ started = Core::Utils::Time.get_time
78
+ wait_time = loop_base_interval + GRACEFUL_SHUTDOWN_EXTRA_SECONDS
79
+
80
+ loop do
81
+ break if buffer.empty? && !in_iteration?
82
+
83
+ sleep(GRACEFUL_SHUTDOWN_WAIT_INTERVAL_SECONDS)
84
+ break if Core::Utils::Time.get_time - started > wait_time
85
+ end
86
+
87
+ stop(true)
88
+ end
89
+
90
+ private
91
+
92
+ def send_events(events, dropped)
93
+ return if events.empty?
94
+
95
+ if dropped.positive?
96
+ @logger.debug { "OpenFeature: Resolution details worker dropped #{dropped} event(s) due to full buffer" }
97
+ end
98
+
99
+ payload = @batch_builder.payload_for(events)
100
+ response = @transport.send_exposures(payload)
101
+
102
+ unless response&.ok?
103
+ @logger.debug { "OpenFeature: Resolution details upload response was not OK: #{response.inspect}" }
104
+ end
105
+
106
+ response
107
+ rescue => e
108
+ @logger.debug { "OpenFeature: Failed to flush resolution details events: #{e.class}: #{e.message}" }
109
+ @telemetry.report(e, description: 'OpenFeature: Failed to flush resolution details events')
110
+
111
+ nil
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Ext
6
+ ERROR = 'ERROR'
7
+ INITIALIZING = 'INITIALIZING'
8
+ UNKNOWN_TYPE = 'UNKNOWN_TYPE'
9
+ GENERAL = 'GENERAL'
10
+ PROVIDER_FATAL = 'PROVIDER_FATAL'
11
+ PROVIDER_NOT_READY = 'PROVIDER_NOT_READY'
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../core/feature_flags'
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ # This class is an interface of evaluation logic using native extension
8
+ class NativeEvaluator
9
+ # NOTE: In a currect implementation configuration is expected to be a raw
10
+ # JSON string containing feature flags (straight from the remote config)
11
+ # in the format expected by `libdatadog` without any modifications
12
+ def initialize(configuration)
13
+ @configuration = Core::FeatureFlags::Configuration.new(configuration)
14
+ end
15
+
16
+ # Returns the assignment for a given flag key based on the feature flags
17
+ # configuration
18
+ #
19
+ # @param flag_key [String] The key of the feature flag
20
+ # @param default_value [Object] The default value to return if the flag is
21
+ # not found or evaluation itself fails
22
+ # @param expected_type [Symbol] The expected type of the flag
23
+ # @param context [Hash] The context of the evaluation, containing targeting key
24
+ # and other attributes
25
+ #
26
+ # @return [Core::FeatureFlags::ResolutionDetails] The assignment for the flag
27
+ def get_assignment(flag_key, default_value:, expected_type:, context:)
28
+ result = @configuration.get_assignment(flag_key, expected_type, context)
29
+
30
+ # NOTE: This is a special case when we need to fallback to the default
31
+ # value, even tho the evaluation itself doesn't produce an error
32
+ # resolution details
33
+ result.value = default_value if result.variant.nil?
34
+ result
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ext'
4
+ require_relative 'resolution_details'
5
+
6
+ module Datadog
7
+ module OpenFeature
8
+ # This class is a noop interface of evaluation logic
9
+ class NoopEvaluator
10
+ def initialize(_configuration)
11
+ # no-op
12
+ end
13
+
14
+ def get_assignment(_flag_key, default_value:, context:, expected_type:)
15
+ ResolutionDetails.new(
16
+ value: default_value,
17
+ log?: false,
18
+ error?: true,
19
+ error_code: Ext::PROVIDER_NOT_READY,
20
+ error_message: 'Waiting for flags configuration',
21
+ reason: Ext::ERROR
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end