datadog 2.43.0 → 2.44.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
  4. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  9. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  12. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  13. data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
  14. data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
  15. data/ext/libdatadog_api/di.c +8 -3
  16. data/ext/libdatadog_api/extconf.rb +1 -1
  17. data/ext/libdatadog_api/feature_flags.c +8 -0
  18. data/ext/libdatadog_api/init.c +3 -1
  19. data/ext/libdatadog_api/otel_thread_context.c +38 -17
  20. data/ext/libdatadog_extconf_helpers.rb +14 -9
  21. data/lib/datadog/ai_guard/component.rb +17 -8
  22. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  23. data/lib/datadog/ai_guard/configuration.rb +13 -0
  24. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  25. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  26. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  27. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  29. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  30. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  31. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  32. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  33. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  34. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  35. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  36. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  37. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  38. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  39. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  40. data/lib/datadog/ai_guard/ext.rb +1 -0
  41. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  42. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  43. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  44. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  45. data/lib/datadog/ai_guard/redaction.rb +80 -0
  46. data/lib/datadog/ai_guard.rb +58 -57
  47. data/lib/datadog/core/configuration/components.rb +9 -1
  48. data/lib/datadog/core/configuration/settings.rb +5 -2
  49. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  50. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  51. data/lib/datadog/core/remote/component.rb +9 -2
  52. data/lib/datadog/core/remote/worker.rb +8 -0
  53. data/lib/datadog/di/instrumenter.rb +1 -1
  54. data/lib/datadog/di/probe_manager.rb +10 -5
  55. data/lib/datadog/di/proc_responder.rb +1 -1
  56. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  57. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  58. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  59. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  60. data/lib/datadog/open_feature/component.rb +57 -1
  61. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  62. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  63. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  64. data/lib/datadog/open_feature/configuration.rb +118 -0
  65. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  66. data/lib/datadog/open_feature/remote.rb +5 -5
  67. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  68. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  69. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  70. data/lib/datadog/symbol_database/extractor.rb +84 -102
  71. data/lib/datadog/tracing/component.rb +2 -0
  72. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  73. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  74. data/lib/datadog/tracing/context.rb +24 -2
  75. data/lib/datadog/tracing/context_provider.rb +9 -5
  76. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  77. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  78. data/lib/datadog/tracing/otel_thread_context.rb +76 -11
  79. data/lib/datadog/tracing/tracer.rb +15 -2
  80. data/lib/datadog/tracing/transport/native.rb +1 -1
  81. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  82. data/lib/datadog/tracing/transport/traces.rb +1 -1
  83. data/lib/datadog/version.rb +1 -1
  84. metadata +23 -7
  85. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "transport"
4
4
  require_relative "evaluation_engine"
5
+ require_relative "../core/utils/time"
5
6
  require_relative "exposures/buffer"
6
7
  require_relative "exposures/worker"
7
8
  require_relative "exposures/deduplicator"
@@ -16,6 +17,10 @@ module Datadog
16
17
  module OpenFeature
17
18
  # This class is the entry point for the OpenFeature component
18
19
  class Component
20
+ CONFIGURATION_READY = :ready
21
+ CONFIGURATION_TIMEOUT = :timeout
22
+ CONFIGURATION_SHUTDOWN = :shutdown
23
+
19
24
  attr_reader :engine, :flag_eval_metrics_hook, :flag_eval_evp_hook, :span_enrichment_hook
20
25
 
21
26
  def self.build(settings, agent_settings, logger:, telemetry:)
@@ -29,7 +34,7 @@ module Datadog
29
34
  return
30
35
  end
31
36
 
32
- if RUBY_ENGINE != "ruby"
37
+ unless ["ruby", "truffleruby"].include?(RUBY_ENGINE)
33
38
  message = "OpenFeature could not be enabled as MRI is required, " \
34
39
  "but running on #{RUBY_ENGINE.inspect}"
35
40
 
@@ -63,9 +68,56 @@ module Datadog
63
68
  @flag_eval_metrics_hook = create_flag_eval_metrics_hook
64
69
  @flag_eval_evp_hook = create_flag_eval_evp_hook
65
70
  @span_enrichment_hook = create_span_enrichment_hook
71
+
72
+ @configuration_mutex = Mutex.new
73
+ @reconfiguration_mutex = Mutex.new
74
+ @configuration_condition = ConditionVariable.new
75
+ @configuration_received = false
76
+ @configuration_shutdown = false
77
+ end
78
+
79
+ def reconfigure!(configuration)
80
+ @reconfiguration_mutex.synchronize do
81
+ return if configuration_shutdown?
82
+
83
+ @engine.reconfigure!(configuration)
84
+
85
+ @configuration_mutex.synchronize do
86
+ return if @configuration_shutdown
87
+
88
+ @configuration_received = !configuration.nil?
89
+ @configuration_condition.broadcast
90
+ end
91
+ end
92
+ end
93
+
94
+ def wait_for_configuration
95
+ timeout_seconds = @settings.feature_flags.initialization_timeout_ms / 1000.0
96
+ deadline = Core::Utils::Time.get_time + timeout_seconds
97
+
98
+ @configuration_mutex.synchronize do
99
+ loop do
100
+ return CONFIGURATION_SHUTDOWN if @configuration_shutdown
101
+ return CONFIGURATION_READY if @configuration_received
102
+
103
+ remaining = deadline - Core::Utils::Time.get_time
104
+ return CONFIGURATION_TIMEOUT unless remaining.positive?
105
+
106
+ @configuration_condition.wait(@configuration_mutex, remaining)
107
+ end
108
+ end
109
+ end
110
+
111
+ def configuration_received?
112
+ @configuration_mutex.synchronize { @configuration_received }
66
113
  end
67
114
 
68
115
  def shutdown!
116
+ @configuration_mutex.synchronize do
117
+ @configuration_shutdown = true
118
+ @configuration_condition.broadcast
119
+ end
120
+
69
121
  @worker.graceful_shutdown
70
122
  @flag_eval_evp_writer&.stop
71
123
  # Symmetric teardown: drop any accumulated span-enrichment state and
@@ -75,6 +127,10 @@ module Datadog
75
127
 
76
128
  private
77
129
 
130
+ def configuration_shutdown?
131
+ @configuration_mutex.synchronize { @configuration_shutdown }
132
+ end
133
+
78
134
  def create_flag_eval_metrics_hook
79
135
  return unless Hooks::FlagEvalMetricsHook.available?
80
136
 
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Configuration
8
+ # Validated endpoint for agentless Feature Flags configuration delivery.
9
+ class AgentlessEndpoint
10
+ DEFAULT_SITE = "datadoghq.com"
11
+ AGENTLESS_SUBDOMAIN = "ufc-server.ff-cdn"
12
+ CONFIGURATION_PATH = "/api/v2/feature-flagging/config/rules-based/server"
13
+ INVALID_SITE_CHARACTERS = /[\s\/?#@:]/
14
+ INTERNAL_WHITESPACE = /\s/
15
+
16
+ class << self
17
+ def build(site:, environment:, base_url: nil, logger: Datadog.logger)
18
+ if base_url
19
+ build_custom(base_url, logger)
20
+ else
21
+ build_managed(site, environment, logger)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def build_custom(base_url, logger)
28
+ unless base_url.ascii_only?
29
+ logger.warn("Feature Flags agentless base URL is invalid; agentless delivery is disabled")
30
+ return
31
+ end
32
+
33
+ if base_url.match?(INTERNAL_WHITESPACE)
34
+ logger.warn("Feature Flags agentless base URL contains whitespace; agentless delivery is disabled")
35
+ return
36
+ end
37
+
38
+ uri = URI.parse(base_url)
39
+ unless uri.is_a?(URI::HTTP) && uri.absolute? && uri.host
40
+ logger.warn("Feature Flags agentless base URL must be an absolute HTTP or HTTPS URL; agentless delivery is disabled")
41
+ return
42
+ end
43
+
44
+ uri.path = CONFIGURATION_PATH if uri.path.to_s.empty? || uri.path == "/"
45
+ new(uri, managed: false)
46
+ rescue URI::InvalidURIError
47
+ logger.warn("Feature Flags agentless base URL is invalid; agentless delivery is disabled")
48
+ nil
49
+ end
50
+
51
+ def build_managed(site, environment, logger)
52
+ string_site = site.to_s
53
+ unless string_site.ascii_only?
54
+ logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
55
+ return
56
+ end
57
+
58
+ normalized_site = string_site.strip.downcase
59
+ normalized_site = DEFAULT_SITE if normalized_site.empty?
60
+ if normalized_site.match?(INVALID_SITE_CHARACTERS)
61
+ logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
62
+ return
63
+ end
64
+
65
+ uri = URI::HTTPS.build(
66
+ host: "#{AGENTLESS_SUBDOMAIN}.#{normalized_site}",
67
+ path: CONFIGURATION_PATH,
68
+ query: environment.nil? ? nil : URI.encode_www_form(dd_env: environment),
69
+ )
70
+ new(uri, managed: true)
71
+ rescue URI::InvalidComponentError
72
+ logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
73
+ nil
74
+ end
75
+ end
76
+
77
+ attr_reader :uri
78
+
79
+ def initialize(uri, managed:)
80
+ @uri = uri
81
+ @managed = managed
82
+ end
83
+
84
+ def managed?
85
+ @managed
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenFeature
5
+ module Configuration
6
+ module Source
7
+ class Resolution
8
+ attr_reader :source
9
+
10
+ def initialize(source, enabled:)
11
+ @source = source
12
+ @enabled = enabled
13
+ end
14
+
15
+ def enabled?
16
+ @enabled
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "source/resolution"
4
+
5
+ module Datadog
6
+ module OpenFeature
7
+ module Configuration
8
+ module Source
9
+ AGENTLESS = "agentless"
10
+ REMOTE_CONFIG = "remote_config"
11
+ OFFLINE = "offline"
12
+
13
+ class << self
14
+ def resolve(settings, logger: Datadog.logger)
15
+ feature_flags = settings.feature_flags
16
+ source = resolve_source(feature_flags, legacy: settings.open_feature, logger: logger)
17
+ enabled = feature_flags.enabled && source != OFFLINE
18
+
19
+ Resolution.new(source, enabled: enabled)
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_source(settings, legacy:, logger:)
25
+ configured_source = settings.configuration_source
26
+ if !settings.using_default?(:configuration_source) && !configured_source.empty?
27
+ return configured_source if configured_source == AGENTLESS ||
28
+ configured_source == REMOTE_CONFIG ||
29
+ configured_source == OFFLINE
30
+
31
+ logger.warn("Unsupported Feature Flags configuration source; Feature Flags are disabled")
32
+ OFFLINE
33
+ elsif !legacy.using_default?(:enabled)
34
+ legacy.enabled ? REMOTE_CONFIG : OFFLINE
35
+ else
36
+ AGENTLESS
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end