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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +36 -5
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +65 -31
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +25 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
- data/ext/datadog_profiling_native_extension/profiling.c +1 -0
- data/ext/datadog_profiling_native_extension/stack_recorder.c +4 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +3 -3
- data/ext/libdatadog_api/di.c +8 -3
- data/ext/libdatadog_api/extconf.rb +1 -1
- data/ext/libdatadog_api/feature_flags.c +8 -0
- data/ext/libdatadog_api/init.c +3 -1
- data/ext/libdatadog_api/otel_thread_context.c +38 -17
- data/ext/libdatadog_extconf_helpers.rb +14 -9
- data/lib/datadog/ai_guard/component.rb +17 -8
- data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
- data/lib/datadog/ai_guard/configuration.rb +13 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
- data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
- data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
- data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
- data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
- data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
- data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
- data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
- data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
- data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
- data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
- data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
- data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
- data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
- data/lib/datadog/ai_guard/evaluation.rb +25 -11
- data/lib/datadog/ai_guard/ext.rb +1 -0
- data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
- data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
- data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
- data/lib/datadog/ai_guard/redaction/result.rb +29 -0
- data/lib/datadog/ai_guard/redaction.rb +80 -0
- data/lib/datadog/ai_guard.rb +58 -57
- data/lib/datadog/core/configuration/components.rb +9 -1
- data/lib/datadog/core/configuration/settings.rb +5 -2
- data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
- data/lib/datadog/core/remote/client/capabilities.rb +10 -4
- data/lib/datadog/core/remote/component.rb +9 -2
- data/lib/datadog/core/remote/worker.rb +8 -0
- data/lib/datadog/di/instrumenter.rb +1 -1
- data/lib/datadog/di/probe_manager.rb +10 -5
- data/lib/datadog/di/proc_responder.rb +1 -1
- data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
- data/lib/datadog/open_feature/agentless/parser.rb +43 -0
- data/lib/datadog/open_feature/agentless/response.rb +19 -0
- data/lib/datadog/open_feature/agentless/transport.rb +103 -0
- data/lib/datadog/open_feature/component.rb +57 -1
- data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
- data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
- data/lib/datadog/open_feature/configuration/source.rb +43 -0
- data/lib/datadog/open_feature/configuration.rb +118 -0
- data/lib/datadog/open_feature/native_evaluator.rb +1 -14
- data/lib/datadog/open_feature/remote.rb +5 -5
- data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
- data/lib/datadog/symbol_database/extractor.rb +84 -102
- data/lib/datadog/tracing/component.rb +2 -0
- data/lib/datadog/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/configuration/settings.rb +6 -0
- data/lib/datadog/tracing/context.rb +24 -2
- data/lib/datadog/tracing/context_provider.rb +9 -5
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
- data/lib/datadog/tracing/metadata/ext.rb +3 -0
- data/lib/datadog/tracing/otel_thread_context.rb +76 -11
- data/lib/datadog/tracing/tracer.rb +15 -2
- data/lib/datadog/tracing/transport/native.rb +1 -1
- data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
- data/lib/datadog/tracing/transport/traces.rb +1 -1
- data/lib/datadog/version.rb +1 -1
- metadata +23 -7
- 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
|
-
|
|
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
|