cw-datadog 2.23.0.2 → 2.23.0.3

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  3. data/ext/libdatadog_api/library_config.c +12 -11
  4. data/ext/libdatadog_extconf_helpers.rb +1 -1
  5. data/lib/datadog/appsec/api_security/route_extractor.rb +20 -5
  6. data/lib/datadog/appsec/api_security/sampler.rb +3 -1
  7. data/lib/datadog/appsec/assets/blocked.html +8 -0
  8. data/lib/datadog/appsec/assets/blocked.json +1 -1
  9. data/lib/datadog/appsec/assets/blocked.text +3 -1
  10. data/lib/datadog/appsec/assets.rb +1 -1
  11. data/lib/datadog/appsec/remote.rb +4 -0
  12. data/lib/datadog/appsec/response.rb +18 -4
  13. data/lib/datadog/core/cloudwise/client.rb +364 -25
  14. data/lib/datadog/core/cloudwise/component.rb +197 -52
  15. data/lib/datadog/core/cloudwise/docc_heartbeat_worker.rb +105 -0
  16. data/lib/datadog/core/cloudwise/docc_operation_worker.rb +191 -0
  17. data/lib/datadog/core/cloudwise/docc_registration_worker.rb +89 -0
  18. data/lib/datadog/core/cloudwise/license_worker.rb +3 -1
  19. data/lib/datadog/core/cloudwise/probe_state.rb +134 -12
  20. data/lib/datadog/core/configuration/components.rb +10 -9
  21. data/lib/datadog/core/configuration/settings.rb +28 -0
  22. data/lib/datadog/core/configuration/supported_configurations.rb +5 -2
  23. data/lib/datadog/core/remote/client/capabilities.rb +7 -0
  24. data/lib/datadog/core/remote/component.rb +2 -2
  25. data/lib/datadog/core/remote/transport/config.rb +2 -10
  26. data/lib/datadog/core/remote/transport/http/config.rb +9 -9
  27. data/lib/datadog/core/remote/transport/http/negotiation.rb +17 -8
  28. data/lib/datadog/core/remote/transport/http.rb +2 -0
  29. data/lib/datadog/core/remote/transport/negotiation.rb +2 -18
  30. data/lib/datadog/core/remote/worker.rb +23 -35
  31. data/lib/datadog/core/telemetry/component.rb +26 -13
  32. data/lib/datadog/core/telemetry/event/app_started.rb +67 -49
  33. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +27 -4
  34. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +5 -6
  35. data/lib/datadog/core/telemetry/transport/telemetry.rb +1 -2
  36. data/lib/datadog/core/telemetry/worker.rb +51 -6
  37. data/lib/datadog/core/transport/http/adapters/net.rb +2 -0
  38. data/lib/datadog/core/transport/http/client.rb +69 -0
  39. data/lib/datadog/core/utils/only_once_successful.rb +6 -2
  40. data/lib/datadog/data_streams/transport/http/client.rb +4 -32
  41. data/lib/datadog/data_streams/transport/stats.rb +1 -1
  42. data/lib/datadog/di/probe_notification_builder.rb +35 -13
  43. data/lib/datadog/di/transport/diagnostics.rb +2 -2
  44. data/lib/datadog/di/transport/http/diagnostics.rb +2 -4
  45. data/lib/datadog/di/transport/http/input.rb +2 -4
  46. data/lib/datadog/di/transport/input.rb +2 -2
  47. data/lib/datadog/open_feature/component.rb +60 -0
  48. data/lib/datadog/open_feature/configuration.rb +27 -0
  49. data/lib/datadog/open_feature/evaluation_engine.rb +59 -0
  50. data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
  51. data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
  52. data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
  53. data/lib/datadog/open_feature/exposures/event.rb +60 -0
  54. data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
  55. data/lib/datadog/open_feature/exposures/worker.rb +116 -0
  56. data/lib/datadog/open_feature/ext.rb +13 -0
  57. data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
  58. data/lib/datadog/open_feature/provider.rb +134 -0
  59. data/lib/datadog/open_feature/remote.rb +74 -0
  60. data/lib/datadog/open_feature/resolution_details.rb +35 -0
  61. data/lib/datadog/open_feature/transport.rb +72 -0
  62. data/lib/datadog/open_feature.rb +19 -0
  63. data/lib/datadog/profiling/component.rb +6 -0
  64. data/lib/datadog/profiling/profiler.rb +4 -0
  65. data/lib/datadog/profiling.rb +1 -2
  66. data/lib/datadog/single_step_instrument.rb +1 -1
  67. data/lib/datadog/tracing/contrib/cloudwise/propagation.rb +164 -7
  68. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +22 -17
  69. data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
  70. data/lib/datadog/tracing/contrib/karafka/patcher.rb +14 -0
  71. data/lib/datadog/tracing/contrib/rack/middlewares.rb +6 -2
  72. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
  73. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
  74. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
  75. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
  76. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
  77. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +46 -0
  78. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
  79. data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
  80. data/lib/datadog/tracing/contrib.rb +1 -0
  81. data/lib/datadog/tracing/transport/http/api.rb +40 -1
  82. data/lib/datadog/tracing/transport/http/client.rb +12 -26
  83. data/lib/datadog/tracing/transport/http/traces.rb +4 -2
  84. data/lib/datadog/tracing/transport/trace_formatter.rb +16 -0
  85. data/lib/datadog/version.rb +2 -2
  86. data/lib/datadog.rb +1 -0
  87. metadata +38 -15
  88. data/lib/datadog/core/cloudwise/IMPLEMENTATION_V2.md +0 -517
  89. data/lib/datadog/core/cloudwise/QUICKSTART.md +0 -398
  90. data/lib/datadog/core/cloudwise/README.md +0 -722
  91. data/lib/datadog/core/remote/transport/http/client.rb +0 -49
  92. data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
  93. data/lib/datadog/di/transport/http/client.rb +0 -47
@@ -1,47 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../../core/transport/http/response'
3
+ require_relative '../../../core/transport/http/client'
4
4
 
5
5
  module Datadog
6
6
  module DataStreams
7
7
  module Transport
8
8
  module HTTP
9
9
  # HTTP client for Data Streams Monitoring
10
- class Client
11
- attr_reader :api, :logger
12
-
13
- def initialize(api, logger:)
14
- @api = api
15
- @logger = logger
16
- end
17
-
10
+ class Client < Core::Transport::HTTP::Client
18
11
  def send_stats_payload(request)
19
12
  send_request(request) do |api, env|
20
- api.send_stats(env)
13
+ # TODO how to make api have the derived type for steep?
14
+ api.send_stats(env) # steep:ignore
21
15
  end
22
16
  end
23
-
24
- private
25
-
26
- def send_request(request, &block)
27
- # Build request into env
28
- env = build_env(request)
29
-
30
- # Get response from API
31
- yield(api, env)
32
- rescue => e
33
- message =
34
- "Internal error during #{self.class.name} request. Cause: #{e.class}: #{e} " \
35
- "Location: #{Array(e.backtrace).first}"
36
-
37
- logger.debug(message)
38
-
39
- Datadog::Core::Transport::InternalErrorResponse.new(e)
40
- end
41
-
42
- def build_env(request)
43
- Datadog::Core::Transport::HTTP::Env.new(request)
44
- end
45
17
  end
46
18
  end
47
19
  end
@@ -34,7 +34,7 @@ module Datadog
34
34
  @default_api = default_api
35
35
  @current_api_id = default_api
36
36
 
37
- @client = HTTP::Client.new(current_api, logger: @logger)
37
+ @client = DataStreams::Transport::HTTP::Client.new(current_api, logger: @logger)
38
38
  end
39
39
 
40
40
  def send_stats(payload)
@@ -116,7 +116,12 @@ module Datadog
116
116
  location = if probe.line?
117
117
  {
118
118
  file: context.path,
119
- lines: [probe.line_no],
119
+ # Line numbers are required to be strings by the
120
+ # system tests schema.
121
+ # Backend I think accepts them also as integers, but some
122
+ # other languages send strings and we decided to require
123
+ # strings for everyone.
124
+ lines: [probe.line_no.to_s],
120
125
  }
121
126
  elsif probe.method?
122
127
  {
@@ -131,19 +136,36 @@ module Datadog
131
136
 
132
137
  {
133
138
  service: settings.service,
134
- "debugger.snapshot": {
135
- id: SecureRandom.uuid,
136
- timestamp: timestamp,
137
- evaluationErrors: evaluation_errors,
138
- probe: {
139
- id: probe.id,
140
- version: 0,
141
- location: location,
139
+ debugger: {
140
+ type: 'snapshot',
141
+ # Product can have three values: di, ld, er.
142
+ # We do not currently implement exception replay.
143
+ # There is currently no specification, and no consensus, for
144
+ # when product should be di (dynamic instrumentation) and when
145
+ # it should be ld (live debugger). I thought the backend was
146
+ # supposed to provide this in probe specification via remote
147
+ # config, but apparently this is not the case and the expectation
148
+ # is that the library figures out the product via heuristics,
149
+ # except there is currently no consensus on said heuristics.
150
+ # .NET always sends ld, other languages send nothing at the moment.
151
+ # Don't send anything for the time being.
152
+ #product: 'di/ld',
153
+ snapshot: {
154
+ id: SecureRandom.uuid,
155
+ timestamp: timestamp,
156
+ evaluationErrors: evaluation_errors,
157
+ probe: {
158
+ id: probe.id,
159
+ version: 0,
160
+ location: location,
161
+ },
162
+ language: 'ruby',
163
+ # TODO add test coverage for callers being nil
164
+ stack: stack,
165
+ # System tests schema validation requires captures to
166
+ # always be present
167
+ captures: captures || {},
142
168
  },
143
- language: 'ruby',
144
- # TODO add test coverage for callers being nil
145
- stack: stack,
146
- captures: captures,
147
169
  },
148
170
  # In python tracer duration is under debugger.snapshot,
149
171
  # but UI appears to expect it here at top level.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../core/transport/parcel'
4
- require_relative 'http/client'
4
+ require_relative 'http/diagnostics'
5
5
 
6
6
  module Datadog
7
7
  module DI
@@ -21,7 +21,7 @@ module Datadog
21
21
  @apis = apis
22
22
  @logger = logger
23
23
 
24
- @client = HTTP::Client.new(current_api, logger: logger)
24
+ @client = DI::Transport::HTTP::Diagnostics::Client.new(current_api, logger: logger)
25
25
  end
26
26
 
27
27
  def current_api
@@ -2,14 +2,14 @@
2
2
 
3
3
  require_relative '../../../core/transport/http/api/instance'
4
4
  require_relative '../../../core/transport/http/api/spec'
5
- require_relative 'client'
5
+ require_relative '../../../core/transport/http/client'
6
6
 
7
7
  module Datadog
8
8
  module DI
9
9
  module Transport
10
10
  module HTTP
11
11
  module Diagnostics
12
- module Client
12
+ class Client < Core::Transport::HTTP::Client
13
13
  def send_diagnostics_payload(request)
14
14
  send_request(request) do |api, env|
15
15
  api.send_diagnostics(env)
@@ -57,8 +57,6 @@ module Datadog
57
57
  end
58
58
  end
59
59
  end
60
-
61
- HTTP::Client.include(Diagnostics::Client)
62
60
  end
63
61
  end
64
62
  end
@@ -2,14 +2,14 @@
2
2
 
3
3
  require_relative '../../../core/transport/http/api/instance'
4
4
  require_relative '../../../core/transport/http/api/spec'
5
- require_relative 'client'
5
+ require_relative '../../../core/transport/http/client'
6
6
 
7
7
  module Datadog
8
8
  module DI
9
9
  module Transport
10
10
  module HTTP
11
11
  module Input
12
- module Client
12
+ class Client < Core::Transport::HTTP::Client
13
13
  def send_input_payload(request)
14
14
  send_request(request) do |api, env|
15
15
  api.send_input(env)
@@ -69,8 +69,6 @@ module Datadog
69
69
  end
70
70
  end
71
71
  end
72
-
73
- HTTP::Client.include(Input::Client)
74
72
  end
75
73
  end
76
74
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../../core/transport/parcel'
4
- require_relative 'http/client'
4
+ require_relative 'http/input'
5
5
 
6
6
  module Datadog
7
7
  module DI
@@ -28,7 +28,7 @@ module Datadog
28
28
  @apis = apis
29
29
  @logger = logger
30
30
 
31
- @client = HTTP::Client.new(current_api, logger: logger)
31
+ @client = DI::Transport::HTTP::Input::Client.new(current_api, logger: logger)
32
32
  end
33
33
 
34
34
  def current_api
@@ -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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ext'
4
+ require_relative 'noop_evaluator'
5
+ require_relative 'resolution_details'
6
+
7
+ module Datadog
8
+ module OpenFeature
9
+ # This class performs the evaluation of the feature flag
10
+ class EvaluationEngine
11
+ ReconfigurationError = Class.new(StandardError)
12
+
13
+ ALLOWED_TYPES = %w[boolean string number float integer object].freeze
14
+
15
+ def initialize(reporter, telemetry:, logger:)
16
+ @reporter = reporter
17
+ @telemetry = telemetry
18
+ @logger = logger
19
+
20
+ @evaluator = NoopEvaluator.new(nil)
21
+ end
22
+
23
+ def fetch_value(flag_key:, default_value:, expected_type:, evaluation_context: nil)
24
+ unless ALLOWED_TYPES.include?(expected_type)
25
+ message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
26
+ return ResolutionDetails.build_error(
27
+ value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
28
+ )
29
+ end
30
+
31
+ context = evaluation_context&.fields || {}
32
+ result = @evaluator.get_assignment(flag_key, default_value, context, expected_type)
33
+
34
+ @reporter.report(result, flag_key: flag_key, context: evaluation_context)
35
+
36
+ result
37
+ rescue => e
38
+ @telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
39
+
40
+ ResolutionDetails.build_error(
41
+ value: default_value, error_code: Ext::PROVIDER_FATAL, error_message: e.message
42
+ )
43
+ end
44
+
45
+ def reconfigure!(configuration)
46
+ @logger.debug('OpenFeature: Removing configuration') if configuration.nil?
47
+
48
+ @evaluator = NoopEvaluator.new(configuration)
49
+ rescue => e
50
+ message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
51
+
52
+ @logger.error("#{message}, error #{e.inspect}")
53
+ @telemetry.report(e, description: message)
54
+
55
+ raise ReconfigurationError, e.message
56
+ end
57
+ end
58
+ end
59
+ 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