datadog 2.43.0 → 2.45.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 +69 -1
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +136 -132
- data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -5
- data/ext/datadog_profiling_native_extension/collectors_gc_profiling_helper.c +0 -2
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +19 -20
- data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.h +5 -1
- data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
- data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
- data/ext/datadog_profiling_native_extension/collectors_thread_context.c +146 -109
- data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
- data/ext/datadog_profiling_native_extension/extconf.rb +6 -2
- data/ext/datadog_profiling_native_extension/heap_recorder.c +2 -2
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +122 -53
- data/ext/datadog_profiling_native_extension/private_vm_api_access.h +6 -2
- data/ext/datadog_profiling_native_extension/profiling.c +22 -23
- data/ext/datadog_profiling_native_extension/ruby_helpers.c +3 -1
- data/ext/datadog_profiling_native_extension/ruby_helpers.h +4 -0
- data/ext/datadog_profiling_native_extension/setup_signal_handler.c +0 -2
- data/ext/datadog_profiling_native_extension/stack_recorder.c +10 -28
- data/ext/datadog_profiling_native_extension/stack_recorder.h +0 -1
- data/ext/datadog_profiling_native_extension/time_helpers.c +2 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +13 -14
- 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_api/trace_exporter.c +77 -11
- 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 +7 -2
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
- data/lib/datadog/profiling/component.rb +2 -3
- data/lib/datadog/profiling/exporter.rb +7 -5
- data/lib/datadog/symbol_database/extractor.rb +84 -102
- data/lib/datadog/tracing/component.rb +3 -1
- data/lib/datadog/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/configuration/settings.rb +9 -2
- 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/contrib/utils/quantization/http.rb +6 -3
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -0
- 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 +23 -22
- 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
|
@@ -757,12 +757,15 @@ module Datadog
|
|
|
757
757
|
val ||= ""
|
|
758
758
|
# maps OpenTelemetry semantic attributes to Datadog tags
|
|
759
759
|
key = case key.downcase
|
|
760
|
-
when "deployment.environment"
|
|
760
|
+
when "deployment.environment.name"
|
|
761
|
+
result["env"] = val
|
|
762
|
+
nil
|
|
763
|
+
when "deployment.environment" then "env" unless result.key?("env")
|
|
761
764
|
when "service.version" then "version"
|
|
762
765
|
when "service.name" then "service"
|
|
763
766
|
else key
|
|
764
767
|
end
|
|
765
|
-
result[key] = val unless key.empty?
|
|
768
|
+
result[key] = val unless key.nil? || key.empty?
|
|
766
769
|
end
|
|
767
770
|
end
|
|
768
771
|
result
|
|
@@ -14,6 +14,7 @@ module Datadog
|
|
|
14
14
|
"DD_AI_GUARD_ENDPOINT",
|
|
15
15
|
"DD_AI_GUARD_MAX_CONTENT_SIZE",
|
|
16
16
|
"DD_AI_GUARD_MAX_MESSAGES_LENGTH",
|
|
17
|
+
"DD_AI_GUARD_REDACTION_ENABLED",
|
|
17
18
|
"DD_AI_GUARD_TIMEOUT",
|
|
18
19
|
"DD_API_KEY",
|
|
19
20
|
"DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE",
|
|
@@ -60,10 +61,16 @@ module Datadog
|
|
|
60
61
|
"DD_ERROR_TRACKING_HANDLED_ERRORS",
|
|
61
62
|
"DD_ERROR_TRACKING_HANDLED_ERRORS_INCLUDE",
|
|
62
63
|
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED",
|
|
64
|
+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
|
|
63
65
|
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED",
|
|
64
66
|
"DD_EXPERIMENTAL_NATIVE_TRANSPORT_ENABLED",
|
|
65
67
|
"DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED",
|
|
66
68
|
"DD_EXTERNAL_ENV",
|
|
69
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE",
|
|
70
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL",
|
|
71
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
|
|
72
|
+
"DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
|
|
73
|
+
"DD_FEATURE_FLAGS_ENABLED",
|
|
67
74
|
"DD_FLAGGING_EVALUATION_COUNTS_ENABLED",
|
|
68
75
|
"DD_GIT_COMMIT_SHA",
|
|
69
76
|
"DD_GIT_REPOSITORY_URL",
|
|
@@ -266,6 +273,7 @@ module Datadog
|
|
|
266
273
|
"DD_TRACE_OPENSEARCH_PEER_SERVICE",
|
|
267
274
|
"DD_TRACE_OPENSEARCH_RESOURCE_PATTERN",
|
|
268
275
|
"DD_TRACE_OPENSEARCH_SERVICE_NAME",
|
|
276
|
+
"DD_TRACE_OTEL_CTX_ENABLED",
|
|
269
277
|
"DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED",
|
|
270
278
|
"DD_TRACE_PEER_SERVICE_MAPPING",
|
|
271
279
|
"DD_TRACE_PG_ANALYTICS_ENABLED",
|
|
@@ -16,14 +16,15 @@ module Datadog
|
|
|
16
16
|
class Capabilities
|
|
17
17
|
attr_reader :capabilities, :receivers, :base64_capabilities
|
|
18
18
|
|
|
19
|
-
def initialize(settings, telemetry)
|
|
19
|
+
def initialize(settings, telemetry, open_feature_component_provider: nil)
|
|
20
|
+
open_feature_component_provider ||= -> {}
|
|
20
21
|
@capabilities = []
|
|
21
22
|
@products = []
|
|
22
23
|
@products_mutex = Mutex.new
|
|
23
24
|
@receivers = []
|
|
24
25
|
@telemetry = telemetry
|
|
25
26
|
|
|
26
|
-
register(settings)
|
|
27
|
+
register(settings, open_feature_component_provider)
|
|
27
28
|
|
|
28
29
|
@base64_capabilities = capabilities_to_base64
|
|
29
30
|
end
|
|
@@ -46,7 +47,7 @@ module Datadog
|
|
|
46
47
|
|
|
47
48
|
private
|
|
48
49
|
|
|
49
|
-
def register(settings)
|
|
50
|
+
def register(settings, open_feature_component_provider)
|
|
50
51
|
if settings.respond_to?(:appsec) && settings.appsec.enabled
|
|
51
52
|
register_capabilities(Datadog::AppSec::Remote.capabilities)
|
|
52
53
|
register_products(Datadog::AppSec::Remote.products)
|
|
@@ -98,7 +99,12 @@ module Datadog
|
|
|
98
99
|
if settings.respond_to?(:open_feature) && settings.open_feature.enabled
|
|
99
100
|
register_capabilities(Datadog::OpenFeature::Remote.capabilities)
|
|
100
101
|
register_products(Datadog::OpenFeature::Remote.products)
|
|
101
|
-
register_receivers(
|
|
102
|
+
register_receivers(
|
|
103
|
+
Datadog::OpenFeature::Remote.receivers(
|
|
104
|
+
@telemetry,
|
|
105
|
+
component_provider: open_feature_component_provider,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
102
108
|
end
|
|
103
109
|
end
|
|
104
110
|
|
|
@@ -96,6 +96,8 @@ module Datadog
|
|
|
96
96
|
# Recreates the remote configuration client after a fork.
|
|
97
97
|
# This ensures each forked process has a unique client ID and fresh state.
|
|
98
98
|
def after_fork
|
|
99
|
+
@worker.after_fork
|
|
100
|
+
@barrier = Barrier.new(@settings.remote.boot_timeout_seconds)
|
|
99
101
|
@client = Client.new(@transport, @capabilities, settings: @settings, logger: @logger)
|
|
100
102
|
@healthy = false
|
|
101
103
|
logger.debug { "remote configuration client recreated after fork: #{@client.id} products: #{@capabilities.products.sort.join(", ")}" }
|
|
@@ -187,10 +189,15 @@ module Datadog
|
|
|
187
189
|
#
|
|
188
190
|
# Those checks are instead performed inside the worker loop.
|
|
189
191
|
# This allows users to upgrade their agent while keeping their application running.
|
|
190
|
-
def build(settings, agent_settings, logger:, telemetry:)
|
|
192
|
+
def build(settings, agent_settings, logger:, telemetry:, open_feature_component_provider: nil)
|
|
191
193
|
return unless settings.remote.enabled
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
capabilities = Client::Capabilities.new(
|
|
196
|
+
settings,
|
|
197
|
+
telemetry,
|
|
198
|
+
open_feature_component_provider: open_feature_component_provider,
|
|
199
|
+
)
|
|
200
|
+
new(settings, capabilities, agent_settings, logger: logger)
|
|
194
201
|
end
|
|
195
202
|
end
|
|
196
203
|
end
|
|
@@ -70,6 +70,14 @@ module Datadog
|
|
|
70
70
|
@started
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Child-only: discard inherited synchronization without starting a thread.
|
|
74
|
+
def after_fork
|
|
75
|
+
@mutex = Mutex.new
|
|
76
|
+
@thr = nil
|
|
77
|
+
@starting = false
|
|
78
|
+
@started = false
|
|
79
|
+
end
|
|
80
|
+
|
|
73
81
|
private
|
|
74
82
|
|
|
75
83
|
def poll(interval)
|
|
@@ -538,7 +538,7 @@ module Datadog
|
|
|
538
538
|
# the context, we won't be able to report anything as
|
|
539
539
|
# the probe notifier builder requires a context.
|
|
540
540
|
begin
|
|
541
|
-
responder.probe_condition_evaluation_failed_callback(context, exc)
|
|
541
|
+
responder.probe_condition_evaluation_failed_callback(context, condition, exc)
|
|
542
542
|
rescue Exception => nested_exc # standard:disable Lint/RescueException
|
|
543
543
|
Datadog::DI.reraise_if_fatal(nested_exc)
|
|
544
544
|
raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
|
|
@@ -310,17 +310,22 @@ module Datadog
|
|
|
310
310
|
#
|
|
311
311
|
# This can happen when the expression references undefined variables,
|
|
312
312
|
# has type mismatches, or encounters runtime errors during evaluation.
|
|
313
|
-
# Rate-limited to 1 notification per second per probe to
|
|
314
|
-
#
|
|
313
|
+
# Rate-limited to 1 notification per second per probe, and subject to
|
|
314
|
+
# the process-wide global snapshot rate limit, to avoid flooding the
|
|
315
|
+
# backend when conditions fail repeatedly.
|
|
315
316
|
#
|
|
316
317
|
# @param context [Context] The execution context containing probe and captured data
|
|
317
|
-
# @param expr [
|
|
318
|
+
# @param expr [EL::Expression] The condition expression that failed
|
|
318
319
|
# @param exc [Exception] The exception raised during condition evaluation
|
|
319
320
|
def probe_condition_evaluation_failed_callback(context, expr, exc)
|
|
320
321
|
probe = context.probe
|
|
321
322
|
if probe.condition_evaluation_failed_rate_limiter&.allow?
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
if instrumenter.global_snapshot_rate_limiter.allow?
|
|
324
|
+
payload = probe_notification_builder.build_condition_evaluation_failed(context, expr, exc)
|
|
325
|
+
probe_notifier_worker.add_snapshot(payload)
|
|
326
|
+
else
|
|
327
|
+
logger.trace { "di: #{probe.type} probe #{probe.id}: skipping condition error snapshot due to global rate limit" }
|
|
328
|
+
end
|
|
324
329
|
end
|
|
325
330
|
end
|
|
326
331
|
|
|
@@ -20,7 +20,7 @@ module Datadog
|
|
|
20
20
|
executed_proc.call(context)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def probe_condition_evaluation_failed_callback(context, exc)
|
|
23
|
+
def probe_condition_evaluation_failed_callback(context, _expr, exc)
|
|
24
24
|
if failed_proc.nil?
|
|
25
25
|
raise NotImplementedError, "Failed proc not provided"
|
|
26
26
|
end
|
|
@@ -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
|