datadog 2.42.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 +67 -2
- data/README.md +3 -0
- data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -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 +80 -36
- 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/heap_recorder.c +13 -0
- data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
- data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
- 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 +17 -4
- data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
- 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 +51 -15
- data/ext/libdatadog_api/trace_exporter.c +642 -22
- 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/open_feature/transport.rb +5 -0
- 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/profiling/collectors/info.rb +2 -6
- 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/contrib/sequel/jdbc_connection_string.rb +140 -0
- data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
- data/lib/datadog/tracing/metadata/ext.rb +3 -0
- data/lib/datadog/tracing/otel_thread_context.rb +78 -9
- data/lib/datadog/tracing/tracer.rb +15 -2
- data/lib/datadog/tracing/transport/native.rb +18 -26
- data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
- data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
- data/lib/datadog/tracing/transport/traces.rb +3 -23
- data/lib/datadog/version.rb +1 -1
- metadata +25 -7
- data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
|
@@ -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
|
|
@@ -5,6 +5,25 @@ module Datadog
|
|
|
5
5
|
module Configuration
|
|
6
6
|
# A settings class for the OpenFeature component.
|
|
7
7
|
module Settings
|
|
8
|
+
DEFAULT_POLL_INTERVAL_SECONDS = 30
|
|
9
|
+
DEFAULT_REQUEST_TIMEOUT_SECONDS = 5
|
|
10
|
+
DEFAULT_INITIALIZATION_TIMEOUT_MS = 30_000
|
|
11
|
+
|
|
12
|
+
MAX_REQUEST_TIMEOUT_SECONDS = 300
|
|
13
|
+
MAX_INITIALIZATION_TIMEOUT_MS = 2_147_483_647
|
|
14
|
+
|
|
15
|
+
def self.configure_integer_option(option, environment_variable:, default:, setting_name:)
|
|
16
|
+
option.type :int
|
|
17
|
+
option.env environment_variable
|
|
18
|
+
option.env_parser do |value|
|
|
19
|
+
Integer(value, 10)
|
|
20
|
+
rescue ArgumentError
|
|
21
|
+
Datadog.logger.warn("#{setting_name} must be an integer; using the default")
|
|
22
|
+
default
|
|
23
|
+
end
|
|
24
|
+
option.default default
|
|
25
|
+
end
|
|
26
|
+
|
|
8
27
|
def self.extended(base)
|
|
9
28
|
base = base.singleton_class unless base.is_a?(Class)
|
|
10
29
|
add_settings!(base)
|
|
@@ -15,10 +34,19 @@ module Datadog
|
|
|
15
34
|
# Steep does not update `self` for this `class_eval` block.
|
|
16
35
|
# @type self: Datadog::Core::Configuration::Base::_DslContext
|
|
17
36
|
settings :open_feature do
|
|
37
|
+
# Deprecated switch retained for existing Remote Configuration adopters.
|
|
18
38
|
option :enabled do |o|
|
|
19
39
|
o.type :bool
|
|
20
40
|
o.env "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"
|
|
21
41
|
o.default false
|
|
42
|
+
o.after_set do |_, _, precedence|
|
|
43
|
+
next if precedence == Core::Configuration::Option::Precedence::DEFAULT
|
|
44
|
+
|
|
45
|
+
Datadog.logger.warn(
|
|
46
|
+
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED is deprecated; use " \
|
|
47
|
+
"DD_FEATURE_FLAGS_ENABLED and DD_FEATURE_FLAGS_CONFIGURATION_SOURCE instead"
|
|
48
|
+
)
|
|
49
|
+
end
|
|
22
50
|
end
|
|
23
51
|
|
|
24
52
|
# Opt-in gate for APM feature-flag span enrichment. When enabled,
|
|
@@ -42,6 +70,96 @@ module Datadog
|
|
|
42
70
|
o.default true
|
|
43
71
|
end
|
|
44
72
|
end
|
|
73
|
+
|
|
74
|
+
settings :feature_flags do
|
|
75
|
+
option :enabled do |o|
|
|
76
|
+
o.type :bool
|
|
77
|
+
o.env "DD_FEATURE_FLAGS_ENABLED"
|
|
78
|
+
o.default true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
option :configuration_source do |o|
|
|
82
|
+
o.type :string
|
|
83
|
+
o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE"
|
|
84
|
+
o.default "agentless"
|
|
85
|
+
o.setter do |value|
|
|
86
|
+
value.to_s.strip.downcase
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
settings :agentless do
|
|
91
|
+
option :base_url do |o|
|
|
92
|
+
o.type :string, nilable: true
|
|
93
|
+
o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL"
|
|
94
|
+
o.skip_telemetry true
|
|
95
|
+
o.setter do |value|
|
|
96
|
+
next unless value
|
|
97
|
+
|
|
98
|
+
stripped_value = value.to_s.strip
|
|
99
|
+
stripped_value.empty? ? nil : stripped_value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
option :poll_interval_seconds do |o|
|
|
104
|
+
Settings.configure_integer_option(
|
|
105
|
+
o,
|
|
106
|
+
environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
|
|
107
|
+
default: Settings::DEFAULT_POLL_INTERVAL_SECONDS,
|
|
108
|
+
setting_name: "Feature Flags agentless poll interval",
|
|
109
|
+
)
|
|
110
|
+
o.setter do |value|
|
|
111
|
+
if value.is_a?(Integer) && value > 0
|
|
112
|
+
value
|
|
113
|
+
else
|
|
114
|
+
Datadog.logger.warn(
|
|
115
|
+
"Feature Flags agentless poll interval must be positive; using the default"
|
|
116
|
+
)
|
|
117
|
+
Settings::DEFAULT_POLL_INTERVAL_SECONDS
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
option :request_timeout_seconds do |o|
|
|
123
|
+
Settings.configure_integer_option(
|
|
124
|
+
o,
|
|
125
|
+
environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
|
|
126
|
+
default: Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
|
127
|
+
setting_name: "Feature Flags agentless request timeout",
|
|
128
|
+
)
|
|
129
|
+
o.setter do |value|
|
|
130
|
+
if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_REQUEST_TIMEOUT_SECONDS
|
|
131
|
+
value
|
|
132
|
+
else
|
|
133
|
+
Datadog.logger.warn(
|
|
134
|
+
"Feature Flags agentless request timeout must be within (0, " \
|
|
135
|
+
"#{Settings::MAX_REQUEST_TIMEOUT_SECONDS}]; using the default"
|
|
136
|
+
)
|
|
137
|
+
Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
option :initialization_timeout_ms do |o|
|
|
144
|
+
Settings.configure_integer_option(
|
|
145
|
+
o,
|
|
146
|
+
environment_variable: "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
|
|
147
|
+
default: Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS,
|
|
148
|
+
setting_name: "Feature Flags provider initialization timeout",
|
|
149
|
+
)
|
|
150
|
+
o.setter do |value|
|
|
151
|
+
if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_INITIALIZATION_TIMEOUT_MS
|
|
152
|
+
value
|
|
153
|
+
else
|
|
154
|
+
Datadog.logger.warn(
|
|
155
|
+
"Feature Flags provider initialization timeout must be within (0, " \
|
|
156
|
+
"#{Settings::MAX_INITIALIZATION_TIMEOUT_MS}]; using the default"
|
|
157
|
+
)
|
|
158
|
+
Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
45
163
|
end
|
|
46
164
|
end
|
|
47
165
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
3
|
require_relative "../core/feature_flags"
|
|
5
4
|
require_relative "ext"
|
|
6
5
|
require_relative "resolution_details"
|
|
@@ -16,7 +15,7 @@ module Datadog
|
|
|
16
15
|
# in the format expected by `libdatadog` without any modifications
|
|
17
16
|
def initialize(configuration)
|
|
18
17
|
@configuration = Core::FeatureFlags::Configuration.new(configuration)
|
|
19
|
-
@observe_full_evaluation_data =
|
|
18
|
+
@observe_full_evaluation_data = @configuration.observe_full_evaluation_data
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
attr_reader :observe_full_evaluation_data
|
|
@@ -42,18 +41,6 @@ module Datadog
|
|
|
42
41
|
|
|
43
42
|
private
|
|
44
43
|
|
|
45
|
-
# Parse observe_full_evaluation_data from the top level of the UFC JSON (a sibling
|
|
46
|
-
# of `environment`). Absent, null, or wrong-typed values return false.
|
|
47
|
-
def parse_observe_full_evaluation_data(configuration)
|
|
48
|
-
return false unless configuration.is_a?(String) && !configuration.empty?
|
|
49
|
-
|
|
50
|
-
parsed = JSON.parse(configuration)
|
|
51
|
-
parsed.is_a?(Hash) && parsed["observeFullEvaluationData"] == true
|
|
52
|
-
rescue
|
|
53
|
-
# This secondary policy parse must not reject configuration accepted by the native evaluator.
|
|
54
|
-
false
|
|
55
|
-
end
|
|
56
|
-
|
|
57
44
|
def build_resolution_details(result, default_value)
|
|
58
45
|
ResolutionDetails.new(
|
|
59
46
|
value: result.variant.nil? ? default_value : result.value,
|
|
@@ -19,11 +19,11 @@ module Datadog
|
|
|
19
19
|
FFE_PRODUCTS
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def receivers(telemetry)
|
|
22
|
+
def receivers(telemetry, component_provider:)
|
|
23
23
|
matcher = Core::Remote::Dispatcher::Matcher::Product.new(FFE_PRODUCTS)
|
|
24
24
|
receiver = Core::Remote::Dispatcher::Receiver.new(matcher) do |repository, changes|
|
|
25
|
-
|
|
26
|
-
next unless
|
|
25
|
+
component = component_provider.call
|
|
26
|
+
next unless component
|
|
27
27
|
|
|
28
28
|
changes.each do |change|
|
|
29
29
|
content = repository[change.path]
|
|
@@ -38,7 +38,7 @@ module Datadog
|
|
|
38
38
|
when :insert, :update
|
|
39
39
|
begin
|
|
40
40
|
# @type var content: Core::Remote::Configuration::Content
|
|
41
|
-
|
|
41
|
+
component.reconfigure!(read_content(content))
|
|
42
42
|
content.applied
|
|
43
43
|
rescue EvaluationEngine::ReconfigurationError => e
|
|
44
44
|
content.errored("Error applying OpenFeature configuration: #{e.class}: #{e.message}")
|
|
@@ -46,7 +46,7 @@ module Datadog
|
|
|
46
46
|
when :delete
|
|
47
47
|
# NOTE: For now, we treat deletion as clearing the configuration
|
|
48
48
|
# In a multi-config scenario, we might track configs per path
|
|
49
|
-
|
|
49
|
+
component.reconfigure!(nil)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
end
|
|
@@ -8,6 +8,7 @@ require_relative "../core/transport/http/api/endpoint"
|
|
|
8
8
|
require_relative "../core/transport/http/api/instance"
|
|
9
9
|
require_relative "../core/transport/parcel"
|
|
10
10
|
require_relative "../core/transport/request"
|
|
11
|
+
require_relative "../version"
|
|
11
12
|
|
|
12
13
|
module Datadog
|
|
13
14
|
module OpenFeature
|
|
@@ -27,6 +28,8 @@ module Datadog
|
|
|
27
28
|
request_env.headers["Content-Type"] = env.request.parcel.content_type
|
|
28
29
|
request_env.headers[Core::EVP::SUBDOMAIN_HEADER_NAME] =
|
|
29
30
|
Core::EVP::EVENT_PLATFORM_INTAKE_SUBDOMAIN
|
|
31
|
+
request_env.headers["DD-EVP-ORIGIN"] = "dd-trace-rb"
|
|
32
|
+
request_env.headers["DD-EVP-ORIGIN-VERSION"] = Datadog::VERSION::STRING
|
|
30
33
|
request_env.body = env.request.parcel.data
|
|
31
34
|
|
|
32
35
|
block.call(request_env)
|
|
@@ -49,6 +52,8 @@ module Datadog
|
|
|
49
52
|
request_env.headers["Content-Type"] = env.request.parcel.content_type
|
|
50
53
|
request_env.headers[Core::EVP::SUBDOMAIN_HEADER_NAME] =
|
|
51
54
|
Core::EVP::EVENT_PLATFORM_INTAKE_SUBDOMAIN
|
|
55
|
+
request_env.headers["DD-EVP-ORIGIN"] = "dd-trace-rb"
|
|
56
|
+
request_env.headers["DD-EVP-ORIGIN-VERSION"] = Datadog::VERSION::STRING
|
|
52
57
|
request_env.body = env.request.parcel.data
|
|
53
58
|
|
|
54
59
|
block.call(request_env)
|
|
@@ -12,23 +12,24 @@ module Datadog
|
|
|
12
12
|
def create_resource
|
|
13
13
|
resource_attributes = {}
|
|
14
14
|
|
|
15
|
-
@settings.tags&.each do |key, value|
|
|
15
|
+
@settings.tags&.each do |key, value|
|
|
16
16
|
otel_key = case key
|
|
17
17
|
when "service" then "service.name"
|
|
18
|
-
when "env"
|
|
18
|
+
when "env", "deployment.environment.name", "deployment.environment" then next
|
|
19
19
|
when "version" then "service.version"
|
|
20
20
|
else key
|
|
21
21
|
end
|
|
22
22
|
resource_attributes[otel_key] = value
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME
|
|
26
|
-
|
|
27
|
-
resource_attributes["
|
|
25
|
+
resource_attributes["service.name"] = @settings.service_without_fallback || resource_attributes["service.name"] || Datadog::Core::Environment::Ext::FALLBACK_SERVICE_NAME
|
|
26
|
+
environment = @settings.env || @settings.tags["env"] || @settings.tags["deployment.environment.name"] || @settings.tags["deployment.environment"]
|
|
27
|
+
resource_attributes["deployment.environment.name"] = environment if environment
|
|
28
|
+
resource_attributes["service.version"] = @settings.version if @settings.version
|
|
28
29
|
|
|
29
|
-
hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings)
|
|
30
|
+
hostname = Datadog::Core::Environment::Socket.resolved_hostname(@settings)
|
|
30
31
|
if hostname
|
|
31
|
-
if hostname == @settings.hostname
|
|
32
|
+
if hostname == @settings.hostname
|
|
32
33
|
resource_attributes["host.name"] = hostname
|
|
33
34
|
elsif !resource_attributes.key?("host.name")
|
|
34
35
|
resource_attributes["host.name"] = hostname
|
|
@@ -41,9 +42,12 @@ module Datadog
|
|
|
41
42
|
# Returns the signal-specific option value when explicitly set,
|
|
42
43
|
# otherwise falls back to the general OTLP exporter config or computed_default.
|
|
43
44
|
def config_or_exporter_fallback(signal:, option_name:, computed_default: nil)
|
|
44
|
-
signal_settings =
|
|
45
|
+
signal_settings = case signal
|
|
46
|
+
when :logs then @settings.opentelemetry.logs
|
|
47
|
+
when :metrics then @settings.opentelemetry.metrics
|
|
48
|
+
end
|
|
45
49
|
if signal_settings.using_default?(option_name)
|
|
46
|
-
@settings.opentelemetry.exporter.public_send(option_name) || computed_default
|
|
50
|
+
@settings.opentelemetry.exporter.public_send(option_name) || computed_default
|
|
47
51
|
else
|
|
48
52
|
signal_settings.public_send(option_name)
|
|
49
53
|
end
|
|
@@ -51,7 +51,7 @@ module Datadog
|
|
|
51
51
|
# profiler overhead!
|
|
52
52
|
dynamic_sampling_rate_enabled: true,
|
|
53
53
|
skip_idle_samples_for_testing: false,
|
|
54
|
-
idle_sampling_helper: IdleSamplingHelper.new
|
|
54
|
+
idle_sampling_helper: IdleSamplingHelper.new
|
|
55
55
|
)
|
|
56
56
|
unless dynamic_sampling_rate_enabled
|
|
57
57
|
Datadog.logger.warn(
|
|
@@ -98,7 +98,7 @@ module Datadog
|
|
|
98
98
|
|
|
99
99
|
Datadog.logger.debug { "Starting thread for: #{self}" }
|
|
100
100
|
|
|
101
|
-
@idle_sampling_helper.start
|
|
101
|
+
@idle_sampling_helper.start(self)
|
|
102
102
|
|
|
103
103
|
@worker_thread = Thread.new do
|
|
104
104
|
Thread.current.name = self.class.name
|