datadog 2.22.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +1 -58
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +4 -0
  5. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +1 -1
  6. data/ext/datadog_profiling_native_extension/extconf.rb +6 -4
  7. data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
  8. data/ext/libdatadog_api/datadog_ruby_common.h +1 -1
  9. data/ext/libdatadog_api/feature_flags.c +554 -0
  10. data/ext/libdatadog_api/feature_flags.h +5 -0
  11. data/ext/libdatadog_api/init.c +2 -0
  12. data/ext/libdatadog_api/library_config.c +12 -11
  13. data/ext/libdatadog_extconf_helpers.rb +1 -1
  14. data/lib/datadog/appsec/api_security/route_extractor.rb +23 -6
  15. data/lib/datadog/appsec/api_security/sampler.rb +7 -4
  16. data/lib/datadog/appsec/assets/blocked.html +8 -0
  17. data/lib/datadog/appsec/assets/blocked.json +1 -1
  18. data/lib/datadog/appsec/assets/blocked.text +3 -1
  19. data/lib/datadog/appsec/assets.rb +1 -1
  20. data/lib/datadog/appsec/remote.rb +4 -0
  21. data/lib/datadog/appsec/response.rb +18 -4
  22. data/lib/datadog/core/configuration/components.rb +30 -3
  23. data/lib/datadog/core/configuration/config_helper.rb +1 -1
  24. data/lib/datadog/core/configuration/settings.rb +14 -0
  25. data/lib/datadog/core/configuration/supported_configurations.rb +330 -301
  26. data/lib/datadog/core/ddsketch.rb +0 -2
  27. data/lib/datadog/core/environment/ext.rb +6 -0
  28. data/lib/datadog/core/environment/process.rb +79 -0
  29. data/lib/datadog/core/feature_flags.rb +61 -0
  30. data/lib/datadog/core/remote/client/capabilities.rb +7 -0
  31. data/lib/datadog/core/remote/transport/config.rb +2 -10
  32. data/lib/datadog/core/remote/transport/http/config.rb +9 -9
  33. data/lib/datadog/core/remote/transport/http/negotiation.rb +17 -8
  34. data/lib/datadog/core/remote/transport/http.rb +2 -0
  35. data/lib/datadog/core/remote/transport/negotiation.rb +2 -18
  36. data/lib/datadog/core/remote/worker.rb +25 -37
  37. data/lib/datadog/core/tag_builder.rb +0 -4
  38. data/lib/datadog/core/tag_normalizer.rb +84 -0
  39. data/lib/datadog/core/telemetry/component.rb +7 -3
  40. data/lib/datadog/core/telemetry/event/app_started.rb +52 -49
  41. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  42. data/lib/datadog/core/telemetry/logger.rb +2 -2
  43. data/lib/datadog/core/telemetry/logging.rb +2 -8
  44. data/lib/datadog/core/telemetry/transport/http/telemetry.rb +5 -6
  45. data/lib/datadog/core/telemetry/transport/telemetry.rb +1 -2
  46. data/lib/datadog/core/transport/http/client.rb +69 -0
  47. data/lib/datadog/core/utils/array.rb +29 -0
  48. data/lib/datadog/{appsec/api_security → core/utils}/lru_cache.rb +10 -21
  49. data/lib/datadog/core/utils/network.rb +3 -1
  50. data/lib/datadog/core/utils/only_once_successful.rb +6 -2
  51. data/lib/datadog/core/utils.rb +2 -0
  52. data/lib/datadog/data_streams/configuration/settings.rb +49 -0
  53. data/lib/datadog/data_streams/configuration.rb +11 -0
  54. data/lib/datadog/data_streams/ext.rb +11 -0
  55. data/lib/datadog/data_streams/extensions.rb +16 -0
  56. data/lib/datadog/data_streams/pathway_context.rb +169 -0
  57. data/lib/datadog/data_streams/processor.rb +509 -0
  58. data/lib/datadog/data_streams/transport/http/api.rb +33 -0
  59. data/lib/datadog/data_streams/transport/http/client.rb +21 -0
  60. data/lib/datadog/data_streams/transport/http/stats.rb +87 -0
  61. data/lib/datadog/data_streams/transport/http.rb +41 -0
  62. data/lib/datadog/data_streams/transport/stats.rb +60 -0
  63. data/lib/datadog/data_streams.rb +100 -0
  64. data/lib/datadog/di/component.rb +0 -16
  65. data/lib/datadog/di/el/evaluator.rb +1 -1
  66. data/lib/datadog/di/error.rb +4 -0
  67. data/lib/datadog/di/instrumenter.rb +76 -30
  68. data/lib/datadog/di/probe.rb +20 -0
  69. data/lib/datadog/di/probe_manager.rb +10 -2
  70. data/lib/datadog/di/probe_notification_builder.rb +62 -23
  71. data/lib/datadog/di/proc_responder.rb +32 -0
  72. data/lib/datadog/di/transport/diagnostics.rb +2 -2
  73. data/lib/datadog/di/transport/http/diagnostics.rb +2 -4
  74. data/lib/datadog/di/transport/http/input.rb +2 -4
  75. data/lib/datadog/di/transport/http.rb +6 -2
  76. data/lib/datadog/di/transport/input.rb +64 -4
  77. data/lib/datadog/open_feature/component.rb +60 -0
  78. data/lib/datadog/open_feature/configuration.rb +27 -0
  79. data/lib/datadog/open_feature/evaluation_engine.rb +69 -0
  80. data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
  81. data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
  82. data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
  83. data/lib/datadog/open_feature/exposures/event.rb +60 -0
  84. data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
  85. data/lib/datadog/open_feature/exposures/worker.rb +116 -0
  86. data/lib/datadog/open_feature/ext.rb +14 -0
  87. data/lib/datadog/open_feature/native_evaluator.rb +38 -0
  88. data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
  89. data/lib/datadog/open_feature/provider.rb +141 -0
  90. data/lib/datadog/open_feature/remote.rb +74 -0
  91. data/lib/datadog/open_feature/resolution_details.rb +35 -0
  92. data/lib/datadog/open_feature/transport.rb +72 -0
  93. data/lib/datadog/open_feature.rb +19 -0
  94. data/lib/datadog/opentelemetry/configuration/settings.rb +159 -0
  95. data/lib/datadog/opentelemetry/metrics.rb +110 -0
  96. data/lib/datadog/opentelemetry/sdk/configurator.rb +25 -1
  97. data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +38 -0
  98. data/lib/datadog/opentelemetry.rb +3 -0
  99. data/lib/datadog/profiling/collectors/code_provenance.rb +15 -6
  100. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
  101. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -1
  102. data/lib/datadog/profiling/profiler.rb +4 -0
  103. data/lib/datadog/profiling/tag_builder.rb +36 -3
  104. data/lib/datadog/profiling.rb +1 -2
  105. data/lib/datadog/single_step_instrument.rb +1 -1
  106. data/lib/datadog/tracing/configuration/ext.rb +9 -0
  107. data/lib/datadog/tracing/configuration/settings.rb +74 -0
  108. data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -4
  109. data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -2
  110. data/lib/datadog/tracing/contrib/active_job/log_injection.rb +21 -7
  111. data/lib/datadog/tracing/contrib/active_job/patcher.rb +5 -1
  112. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +4 -2
  113. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -1
  114. data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +11 -3
  115. data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +11 -7
  116. data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +7 -3
  117. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +22 -17
  118. data/lib/datadog/tracing/contrib/http/configuration/settings.rb +11 -3
  119. data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +11 -3
  120. data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +11 -3
  121. data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +66 -0
  122. data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +66 -0
  123. data/lib/datadog/tracing/contrib/kafka/patcher.rb +14 -0
  124. data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
  125. data/lib/datadog/tracing/contrib/karafka/monitor.rb +11 -0
  126. data/lib/datadog/tracing/contrib/karafka/patcher.rb +32 -0
  127. data/lib/datadog/tracing/contrib/rack/middlewares.rb +59 -27
  128. data/lib/datadog/tracing/contrib/rack/route_inference.rb +53 -0
  129. data/lib/datadog/tracing/contrib/rails/middlewares.rb +2 -2
  130. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +4 -1
  131. data/lib/datadog/tracing/contrib/roda/instrumentation.rb +3 -1
  132. data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +3 -1
  133. data/lib/datadog/tracing/contrib/status_range_matcher.rb +7 -0
  134. data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
  135. data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
  136. data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
  137. data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
  138. data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
  139. data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +46 -0
  140. data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
  141. data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
  142. data/lib/datadog/tracing/contrib.rb +1 -0
  143. data/lib/datadog/tracing/metadata/ext.rb +1 -1
  144. data/lib/datadog/tracing/transport/http/client.rb +12 -26
  145. data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
  146. data/lib/datadog/tracing/transport/traces.rb +3 -5
  147. data/lib/datadog/version.rb +2 -2
  148. data/lib/datadog.rb +2 -0
  149. metadata +78 -15
  150. data/lib/datadog/core/remote/transport/http/client.rb +0 -49
  151. data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
  152. data/lib/datadog/di/transport/http/client.rb +0 -47
@@ -87,10 +87,41 @@ module Datadog
87
87
  end
88
88
  end
89
89
 
90
+ message = nil
91
+ evaluation_errors = []
92
+ if segments = probe.template_segments
93
+ message, evaluation_errors = evaluate_template(segments, context)
94
+ end
95
+ build_snapshot_base(context,
96
+ evaluation_errors: evaluation_errors, message: message,
97
+ captures: captures)
98
+ end
99
+
100
+ def build_condition_evaluation_failed(context, expression, exception)
101
+ error = {
102
+ message: "#{exception.class}: #{exception}",
103
+ expr: expression.dsl_expr,
104
+ }
105
+ build_snapshot_base(context, evaluation_errors: [error])
106
+ end
107
+
108
+ private
109
+
110
+ def build_snapshot_base(context, evaluation_errors: [], captures: nil, message: nil)
111
+ probe = context.probe
112
+
113
+ timestamp = timestamp_now
114
+ duration = context.duration
115
+
90
116
  location = if probe.line?
91
117
  {
92
118
  file: context.path,
93
- 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],
94
125
  }
95
126
  elsif probe.method?
96
127
  {
@@ -103,28 +134,38 @@ module Datadog
103
134
  format_caller_locations(caller_locations)
104
135
  end
105
136
 
106
- timestamp = timestamp_now
107
- message = nil
108
- evaluation_errors = []
109
- if segments = probe.template_segments
110
- message, evaluation_errors = evaluate_template(segments, context)
111
- end
112
- duration = context.duration
113
137
  {
114
138
  service: settings.service,
115
- "debugger.snapshot": {
116
- id: SecureRandom.uuid,
117
- timestamp: timestamp,
118
- evaluationErrors: evaluation_errors,
119
- probe: {
120
- id: probe.id,
121
- version: 0,
122
- 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 || {},
123
168
  },
124
- language: 'ruby',
125
- # TODO add test coverage for callers being nil
126
- stack: stack,
127
- captures: captures,
128
169
  },
129
170
  # In python tracer duration is under debugger.snapshot,
130
171
  # but UI appears to expect it here at top level.
@@ -132,7 +173,7 @@ module Datadog
132
173
  host: nil,
133
174
  logger: {
134
175
  name: probe.file,
135
- method: probe.method_name || 'no_method',
176
+ method: probe.method_name,
136
177
  thread_name: Thread.current.name,
137
178
  # Dynamic instrumentation currently does not need thread_id for
138
179
  # anything. It can be sent if a customer requests it at which point
@@ -150,8 +191,6 @@ module Datadog
150
191
  }
151
192
  end
152
193
 
153
- private
154
-
155
194
  def build_status(probe, message:, status:)
156
195
  {
157
196
  service: settings.service,
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ # An adapter to convert procs to responders.
6
+ #
7
+ # Used in test suite and benchmarks.
8
+ #
9
+ # @api private
10
+ class ProcResponder
11
+ def initialize(executed_proc, failed_proc = nil)
12
+ @executed_proc = executed_proc
13
+ @failed_proc = failed_proc
14
+ end
15
+
16
+ attr_reader :executed_proc
17
+ attr_reader :failed_proc
18
+
19
+ def probe_executed_callback(context)
20
+ executed_proc.call(context)
21
+ end
22
+
23
+ def probe_condition_evaluation_failed_callback(context, exc)
24
+ if failed_proc.nil?
25
+ raise NotImplementedError, "Failed proc not provided"
26
+ end
27
+
28
+ failed_proc.call(context, exc)
29
+ end
30
+ end
31
+ end
32
+ 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/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
@@ -40,9 +40,13 @@ module Datadog
40
40
  api_version: nil,
41
41
  headers: nil
42
42
  )
43
- Core::Transport::HTTP.build(api_instance_class: Input::API::Instance,
43
+ Core::Transport::HTTP.build(
44
+ api_instance_class: Input::API::Instance,
44
45
  logger: logger,
45
- agent_settings: agent_settings, api_version: api_version, headers: headers) do |transport|
46
+ agent_settings: agent_settings,
47
+ api_version: api_version,
48
+ headers: headers,
49
+ ) do |transport|
46
50
  apis = API.defaults
47
51
 
48
52
  transport.api API::INPUT, apis[API::INPUT]
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../core/chunker'
4
+ require_relative '../../core/encoding'
5
+ require_relative '../../core/tag_builder'
3
6
  require_relative '../../core/transport/parcel'
4
- require_relative 'http/client'
7
+ require_relative '../../core/transport/request'
8
+ require_relative '../error'
9
+ require_relative 'http/input'
5
10
 
6
11
  module Datadog
7
12
  module DI
@@ -24,11 +29,30 @@ module Datadog
24
29
  class Transport
25
30
  attr_reader :client, :apis, :default_api, :current_api_id, :logger
26
31
 
32
+ # The limit on an individual snapshot payload, aka "log line",
33
+ # is 1 MB.
34
+ #
35
+ # TODO There is an RFC for snapshot pruning that should be
36
+ # implemented to reduce the size of snapshots to be below this
37
+ # limit, so that we can send a portion of the captured data
38
+ # rather than dropping the snapshot entirely.
39
+ MAX_SERIALIZED_SNAPSHOT_SIZE = 1024 * 1024
40
+
41
+ # The maximum chunk (batch) size that intake permits is 5 MB.
42
+ #
43
+ # Two bytes are for the [ and ] of JSON array syntax.
44
+ MAX_CHUNK_SIZE = 5 * 1024 * 1024 - 2
45
+
46
+ # Try to send smaller payloads to avoid large network requests.
47
+ # If a payload is larger than default chunk size but is under the
48
+ # max chunk size, it will still get sent out.
49
+ DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024
50
+
27
51
  def initialize(apis, default_api, logger:)
28
52
  @apis = apis
29
53
  @logger = logger
30
54
 
31
- @client = HTTP::Client.new(current_api, logger: logger)
55
+ @client = DI::Transport::HTTP::Input::Client.new(current_api, logger: logger)
32
56
  end
33
57
 
34
58
  def current_api
@@ -36,9 +60,45 @@ module Datadog
36
60
  end
37
61
 
38
62
  def send_input(payload, tags)
39
- json = JSON.dump(payload)
40
- parcel = EncodedParcel.new(json)
63
+ # Tags are the same for all chunks, serialize them one time.
41
64
  serialized_tags = Core::TagBuilder.serialize_tags(tags)
65
+
66
+ encoder = Core::Encoding::JSONEncoder
67
+ encoded_snapshots = Core::Utils::Array.filter_map(payload) do |snapshot|
68
+ encoded = encoder.encode(snapshot)
69
+ if encoded.length > MAX_SERIALIZED_SNAPSHOT_SIZE
70
+ # Drop the snapshot.
71
+ # TODO report via telemetry metric?
72
+ logger.debug { "di: dropping too big snapshot" }
73
+ nil
74
+ else
75
+ encoded
76
+ end
77
+ end
78
+
79
+ Datadog::Core::Chunker.chunk_by_size(
80
+ encoded_snapshots, DEFAULT_CHUNK_SIZE,
81
+ ).each do |chunk|
82
+ # We drop snapshots that are too big earlier.
83
+ # The limit on chunked payload length here is greater
84
+ # than the limit on snapshot size, therefore no chunks
85
+ # can exceed limits here.
86
+ chunked_payload = encoder.join(chunk)
87
+
88
+ # We need to rescue exceptions for each chunk so that
89
+ # subsequent chunks are attempted to be sent.
90
+ begin
91
+ send_input_chunk(chunked_payload, serialized_tags)
92
+ rescue => exc
93
+ logger.debug { "di: failed to send snapshot chunk: #{exc.class}: #{exc} (at #{exc.backtrace.first})" }
94
+ end
95
+ end
96
+
97
+ payload
98
+ end
99
+
100
+ def send_input_chunk(chunked_payload, serialized_tags)
101
+ parcel = EncodedParcel.new(chunked_payload)
42
102
  request = Request.new(parcel, serialized_tags)
43
103
 
44
104
  response = @client.send_input_payload(request)
@@ -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