datadog 2.22.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -2
- data/ext/LIBDATADOG_DEVELOPMENT.md +1 -58
- data/ext/datadog_profiling_native_extension/collectors_stack.c +4 -0
- data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +1 -1
- data/ext/datadog_profiling_native_extension/extconf.rb +6 -4
- data/ext/datadog_profiling_native_extension/heap_recorder.c +1 -1
- data/ext/libdatadog_api/datadog_ruby_common.h +1 -1
- data/ext/libdatadog_api/feature_flags.c +554 -0
- data/ext/libdatadog_api/feature_flags.h +5 -0
- data/ext/libdatadog_api/init.c +2 -0
- data/ext/libdatadog_api/library_config.c +12 -11
- data/ext/libdatadog_extconf_helpers.rb +1 -1
- data/lib/datadog/appsec/api_security/route_extractor.rb +23 -6
- data/lib/datadog/appsec/api_security/sampler.rb +7 -4
- data/lib/datadog/appsec/assets/blocked.html +8 -0
- data/lib/datadog/appsec/assets/blocked.json +1 -1
- data/lib/datadog/appsec/assets/blocked.text +3 -1
- data/lib/datadog/appsec/assets.rb +1 -1
- data/lib/datadog/appsec/remote.rb +4 -0
- data/lib/datadog/appsec/response.rb +18 -4
- data/lib/datadog/core/configuration/components.rb +30 -3
- data/lib/datadog/core/configuration/config_helper.rb +1 -1
- data/lib/datadog/core/configuration/settings.rb +14 -0
- data/lib/datadog/core/configuration/supported_configurations.rb +330 -301
- data/lib/datadog/core/ddsketch.rb +0 -2
- data/lib/datadog/core/environment/ext.rb +6 -0
- data/lib/datadog/core/environment/process.rb +79 -0
- data/lib/datadog/core/feature_flags.rb +61 -0
- data/lib/datadog/core/remote/client/capabilities.rb +7 -0
- data/lib/datadog/core/remote/transport/config.rb +2 -10
- data/lib/datadog/core/remote/transport/http/config.rb +9 -9
- data/lib/datadog/core/remote/transport/http/negotiation.rb +17 -8
- data/lib/datadog/core/remote/transport/http.rb +2 -0
- data/lib/datadog/core/remote/transport/negotiation.rb +2 -18
- data/lib/datadog/core/remote/worker.rb +25 -37
- data/lib/datadog/core/tag_builder.rb +0 -4
- data/lib/datadog/core/tag_normalizer.rb +84 -0
- data/lib/datadog/core/telemetry/component.rb +7 -3
- data/lib/datadog/core/telemetry/event/app_started.rb +52 -49
- data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
- data/lib/datadog/core/telemetry/logger.rb +2 -2
- data/lib/datadog/core/telemetry/logging.rb +2 -8
- data/lib/datadog/core/telemetry/transport/http/telemetry.rb +5 -6
- data/lib/datadog/core/telemetry/transport/telemetry.rb +1 -2
- data/lib/datadog/core/transport/http/client.rb +69 -0
- data/lib/datadog/core/utils/array.rb +29 -0
- data/lib/datadog/{appsec/api_security → core/utils}/lru_cache.rb +10 -21
- data/lib/datadog/core/utils/network.rb +3 -1
- data/lib/datadog/core/utils/only_once_successful.rb +6 -2
- data/lib/datadog/core/utils.rb +2 -0
- data/lib/datadog/data_streams/configuration/settings.rb +49 -0
- data/lib/datadog/data_streams/configuration.rb +11 -0
- data/lib/datadog/data_streams/ext.rb +11 -0
- data/lib/datadog/data_streams/extensions.rb +16 -0
- data/lib/datadog/data_streams/pathway_context.rb +169 -0
- data/lib/datadog/data_streams/processor.rb +509 -0
- data/lib/datadog/data_streams/transport/http/api.rb +33 -0
- data/lib/datadog/data_streams/transport/http/client.rb +21 -0
- data/lib/datadog/data_streams/transport/http/stats.rb +87 -0
- data/lib/datadog/data_streams/transport/http.rb +41 -0
- data/lib/datadog/data_streams/transport/stats.rb +60 -0
- data/lib/datadog/data_streams.rb +100 -0
- data/lib/datadog/di/component.rb +0 -16
- data/lib/datadog/di/el/evaluator.rb +1 -1
- data/lib/datadog/di/error.rb +4 -0
- data/lib/datadog/di/instrumenter.rb +76 -30
- data/lib/datadog/di/probe.rb +20 -0
- data/lib/datadog/di/probe_manager.rb +10 -2
- data/lib/datadog/di/probe_notification_builder.rb +62 -23
- data/lib/datadog/di/proc_responder.rb +32 -0
- data/lib/datadog/di/transport/diagnostics.rb +2 -2
- data/lib/datadog/di/transport/http/diagnostics.rb +2 -4
- data/lib/datadog/di/transport/http/input.rb +2 -4
- data/lib/datadog/di/transport/http.rb +6 -2
- data/lib/datadog/di/transport/input.rb +64 -4
- data/lib/datadog/open_feature/component.rb +60 -0
- data/lib/datadog/open_feature/configuration.rb +27 -0
- data/lib/datadog/open_feature/evaluation_engine.rb +69 -0
- data/lib/datadog/open_feature/exposures/batch_builder.rb +32 -0
- data/lib/datadog/open_feature/exposures/buffer.rb +43 -0
- data/lib/datadog/open_feature/exposures/deduplicator.rb +30 -0
- data/lib/datadog/open_feature/exposures/event.rb +60 -0
- data/lib/datadog/open_feature/exposures/reporter.rb +40 -0
- data/lib/datadog/open_feature/exposures/worker.rb +116 -0
- data/lib/datadog/open_feature/ext.rb +14 -0
- data/lib/datadog/open_feature/native_evaluator.rb +38 -0
- data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
- data/lib/datadog/open_feature/provider.rb +141 -0
- data/lib/datadog/open_feature/remote.rb +74 -0
- data/lib/datadog/open_feature/resolution_details.rb +35 -0
- data/lib/datadog/open_feature/transport.rb +72 -0
- data/lib/datadog/open_feature.rb +19 -0
- data/lib/datadog/opentelemetry/configuration/settings.rb +159 -0
- data/lib/datadog/opentelemetry/metrics.rb +110 -0
- data/lib/datadog/opentelemetry/sdk/configurator.rb +25 -1
- data/lib/datadog/opentelemetry/sdk/metrics_exporter.rb +38 -0
- data/lib/datadog/opentelemetry.rb +3 -0
- data/lib/datadog/profiling/collectors/code_provenance.rb +15 -6
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +1 -1
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -1
- data/lib/datadog/profiling/profiler.rb +4 -0
- data/lib/datadog/profiling/tag_builder.rb +36 -3
- data/lib/datadog/profiling.rb +1 -2
- data/lib/datadog/single_step_instrument.rb +1 -1
- data/lib/datadog/tracing/configuration/ext.rb +9 -0
- data/lib/datadog/tracing/configuration/settings.rb +74 -0
- data/lib/datadog/tracing/contrib/action_pack/action_controller/instrumentation.rb +4 -4
- data/lib/datadog/tracing/contrib/action_pack/utils.rb +1 -2
- data/lib/datadog/tracing/contrib/active_job/log_injection.rb +21 -7
- data/lib/datadog/tracing/contrib/active_job/patcher.rb +5 -1
- data/lib/datadog/tracing/contrib/aws/instrumentation.rb +4 -2
- data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +4 -1
- data/lib/datadog/tracing/contrib/excon/configuration/settings.rb +11 -3
- data/lib/datadog/tracing/contrib/faraday/configuration/settings.rb +11 -7
- data/lib/datadog/tracing/contrib/grape/configuration/settings.rb +7 -3
- data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +22 -17
- data/lib/datadog/tracing/contrib/http/configuration/settings.rb +11 -3
- data/lib/datadog/tracing/contrib/httpclient/configuration/settings.rb +11 -3
- data/lib/datadog/tracing/contrib/httprb/configuration/settings.rb +11 -3
- data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +66 -0
- data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +66 -0
- data/lib/datadog/tracing/contrib/kafka/patcher.rb +14 -0
- data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
- data/lib/datadog/tracing/contrib/karafka/monitor.rb +11 -0
- data/lib/datadog/tracing/contrib/karafka/patcher.rb +32 -0
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +59 -27
- data/lib/datadog/tracing/contrib/rack/route_inference.rb +53 -0
- data/lib/datadog/tracing/contrib/rails/middlewares.rb +2 -2
- data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +4 -1
- data/lib/datadog/tracing/contrib/roda/instrumentation.rb +3 -1
- data/lib/datadog/tracing/contrib/sinatra/tracer_middleware.rb +3 -1
- data/lib/datadog/tracing/contrib/status_range_matcher.rb +7 -0
- data/lib/datadog/tracing/contrib/waterdrop/configuration/settings.rb +27 -0
- data/lib/datadog/tracing/contrib/waterdrop/distributed/propagation.rb +48 -0
- data/lib/datadog/tracing/contrib/waterdrop/ext.rb +17 -0
- data/lib/datadog/tracing/contrib/waterdrop/integration.rb +43 -0
- data/lib/datadog/tracing/contrib/waterdrop/middleware.rb +46 -0
- data/lib/datadog/tracing/contrib/waterdrop/patcher.rb +46 -0
- data/lib/datadog/tracing/contrib/waterdrop/producer.rb +50 -0
- data/lib/datadog/tracing/contrib/waterdrop.rb +37 -0
- data/lib/datadog/tracing/contrib.rb +1 -0
- data/lib/datadog/tracing/metadata/ext.rb +1 -1
- data/lib/datadog/tracing/transport/http/client.rb +12 -26
- data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
- data/lib/datadog/tracing/transport/traces.rb +3 -5
- data/lib/datadog/version.rb +2 -2
- data/lib/datadog.rb +2 -0
- metadata +78 -15
- data/lib/datadog/core/remote/transport/http/client.rb +0 -49
- data/lib/datadog/core/telemetry/transport/http/client.rb +0 -49
- data/lib/datadog/di/transport/http/client.rb +0 -47
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'event'
|
|
4
|
+
require_relative 'deduplicator'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module OpenFeature
|
|
8
|
+
module Exposures
|
|
9
|
+
# This class is responsible for reporting exposures to the Agent
|
|
10
|
+
class Reporter
|
|
11
|
+
def initialize(worker, telemetry:, logger:)
|
|
12
|
+
@worker = worker
|
|
13
|
+
@logger = logger
|
|
14
|
+
@telemetry = telemetry
|
|
15
|
+
@deduplicator = Deduplicator.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# NOTE: Reporting expects evaluation context to be always present, but it
|
|
19
|
+
# might be missing depending on the customer way of using flags evaluation API.
|
|
20
|
+
# In addition to that the evaluation result must be marked for reporting.
|
|
21
|
+
def report(result, flag_key:, context:)
|
|
22
|
+
return false if context.nil?
|
|
23
|
+
return false unless result.log?
|
|
24
|
+
|
|
25
|
+
key = Event.cache_key(result, flag_key: flag_key, context: context)
|
|
26
|
+
value = Event.cache_value(result, flag_key: flag_key, context: context)
|
|
27
|
+
return false if @deduplicator.duplicate?(key, value)
|
|
28
|
+
|
|
29
|
+
event = Event.build(result, flag_key: flag_key, context: context)
|
|
30
|
+
@worker.enqueue(event)
|
|
31
|
+
rescue => e
|
|
32
|
+
@logger.debug { "OpenFeature: Failed to report resolution details: #{e.class}: #{e.message}" }
|
|
33
|
+
@telemetry.report(e, description: 'OpenFeature: Failed to report resolution details')
|
|
34
|
+
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/utils/time'
|
|
4
|
+
require_relative '../../core/workers/queue'
|
|
5
|
+
require_relative '../../core/workers/polling'
|
|
6
|
+
|
|
7
|
+
require_relative 'buffer'
|
|
8
|
+
require_relative 'batch_builder'
|
|
9
|
+
|
|
10
|
+
module Datadog
|
|
11
|
+
module OpenFeature
|
|
12
|
+
module Exposures
|
|
13
|
+
# This class is responsible for sending exposures to the Agent
|
|
14
|
+
class Worker
|
|
15
|
+
include Core::Workers::Queue
|
|
16
|
+
include Core::Workers::Polling
|
|
17
|
+
|
|
18
|
+
GRACEFUL_SHUTDOWN_EXTRA_SECONDS = 5
|
|
19
|
+
GRACEFUL_SHUTDOWN_WAIT_INTERVAL_SECONDS = 0.5
|
|
20
|
+
|
|
21
|
+
DEFAULT_FLUSH_INTERVAL_SECONDS = 30
|
|
22
|
+
DEFAULT_BUFFER_LIMIT = Buffer::DEFAULT_LIMIT
|
|
23
|
+
|
|
24
|
+
def initialize(
|
|
25
|
+
settings:,
|
|
26
|
+
transport:,
|
|
27
|
+
telemetry:,
|
|
28
|
+
logger:,
|
|
29
|
+
flush_interval_seconds: DEFAULT_FLUSH_INTERVAL_SECONDS,
|
|
30
|
+
buffer_limit: DEFAULT_BUFFER_LIMIT
|
|
31
|
+
)
|
|
32
|
+
@logger = logger
|
|
33
|
+
@transport = transport
|
|
34
|
+
@telemetry = telemetry
|
|
35
|
+
@batch_builder = BatchBuilder.new(settings)
|
|
36
|
+
@buffer_limit = buffer_limit
|
|
37
|
+
|
|
38
|
+
self.buffer = Buffer.new(buffer_limit)
|
|
39
|
+
self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
|
|
40
|
+
self.loop_base_interval = flush_interval_seconds
|
|
41
|
+
self.enabled = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def start
|
|
45
|
+
return if !enabled? || running?
|
|
46
|
+
|
|
47
|
+
perform
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stop(force_stop = false, timeout = Core::Workers::Polling::DEFAULT_SHUTDOWN_TIMEOUT)
|
|
51
|
+
buffer.close if running?
|
|
52
|
+
|
|
53
|
+
super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def enqueue(event)
|
|
57
|
+
buffer.push(event)
|
|
58
|
+
start unless running?
|
|
59
|
+
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dequeue
|
|
64
|
+
[buffer.pop, buffer.dropped_count]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def perform(*args)
|
|
68
|
+
events, dropped = args
|
|
69
|
+
send_events(Array(events), dropped.to_i)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def graceful_shutdown
|
|
73
|
+
return false unless enabled? || !run_loop?
|
|
74
|
+
|
|
75
|
+
self.enabled = false
|
|
76
|
+
|
|
77
|
+
started = Core::Utils::Time.get_time
|
|
78
|
+
wait_time = loop_base_interval + GRACEFUL_SHUTDOWN_EXTRA_SECONDS
|
|
79
|
+
|
|
80
|
+
loop do
|
|
81
|
+
break if buffer.empty? && !in_iteration?
|
|
82
|
+
|
|
83
|
+
sleep(GRACEFUL_SHUTDOWN_WAIT_INTERVAL_SECONDS)
|
|
84
|
+
break if Core::Utils::Time.get_time - started > wait_time
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
stop(true)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def send_events(events, dropped)
|
|
93
|
+
return if events.empty?
|
|
94
|
+
|
|
95
|
+
if dropped.positive?
|
|
96
|
+
@logger.debug { "OpenFeature: Resolution details worker dropped #{dropped} event(s) due to full buffer" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
payload = @batch_builder.payload_for(events)
|
|
100
|
+
response = @transport.send_exposures(payload)
|
|
101
|
+
|
|
102
|
+
unless response&.ok?
|
|
103
|
+
@logger.debug { "OpenFeature: Resolution details upload response was not OK: #{response.inspect}" }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
response
|
|
107
|
+
rescue => e
|
|
108
|
+
@logger.debug { "OpenFeature: Failed to flush resolution details events: #{e.class}: #{e.message}" }
|
|
109
|
+
@telemetry.report(e, description: 'OpenFeature: Failed to flush resolution details events')
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module OpenFeature
|
|
5
|
+
module Ext
|
|
6
|
+
ERROR = 'ERROR'
|
|
7
|
+
INITIALIZING = 'INITIALIZING'
|
|
8
|
+
UNKNOWN_TYPE = 'UNKNOWN_TYPE'
|
|
9
|
+
GENERAL = 'GENERAL'
|
|
10
|
+
PROVIDER_FATAL = 'PROVIDER_FATAL'
|
|
11
|
+
PROVIDER_NOT_READY = 'PROVIDER_NOT_READY'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../core/feature_flags'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
# This class is an interface of evaluation logic using native extension
|
|
8
|
+
class NativeEvaluator
|
|
9
|
+
# NOTE: In a currect implementation configuration is expected to be a raw
|
|
10
|
+
# JSON string containing feature flags (straight from the remote config)
|
|
11
|
+
# in the format expected by `libdatadog` without any modifications
|
|
12
|
+
def initialize(configuration)
|
|
13
|
+
@configuration = Core::FeatureFlags::Configuration.new(configuration)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the assignment for a given flag key based on the feature flags
|
|
17
|
+
# configuration
|
|
18
|
+
#
|
|
19
|
+
# @param flag_key [String] The key of the feature flag
|
|
20
|
+
# @param default_value [Object] The default value to return if the flag is
|
|
21
|
+
# not found or evaluation itself fails
|
|
22
|
+
# @param expected_type [Symbol] The expected type of the flag
|
|
23
|
+
# @param context [Hash] The context of the evaluation, containing targeting key
|
|
24
|
+
# and other attributes
|
|
25
|
+
#
|
|
26
|
+
# @return [Core::FeatureFlags::ResolutionDetails] The assignment for the flag
|
|
27
|
+
def get_assignment(flag_key, default_value:, expected_type:, context:)
|
|
28
|
+
result = @configuration.get_assignment(flag_key, expected_type, context)
|
|
29
|
+
|
|
30
|
+
# NOTE: This is a special case when we need to fallback to the default
|
|
31
|
+
# value, even tho the evaluation itself doesn't produce an error
|
|
32
|
+
# resolution details
|
|
33
|
+
result.value = default_value if result.variant.nil?
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ext'
|
|
4
|
+
require_relative 'resolution_details'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module OpenFeature
|
|
8
|
+
# This class is a noop interface of evaluation logic
|
|
9
|
+
class NoopEvaluator
|
|
10
|
+
def initialize(_configuration)
|
|
11
|
+
# no-op
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get_assignment(_flag_key, default_value:, context:, expected_type:)
|
|
15
|
+
ResolutionDetails.new(
|
|
16
|
+
value: default_value,
|
|
17
|
+
log?: false,
|
|
18
|
+
error?: true,
|
|
19
|
+
error_code: Ext::PROVIDER_NOT_READY,
|
|
20
|
+
error_message: 'Waiting for flags configuration',
|
|
21
|
+
reason: Ext::ERROR
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ext'
|
|
4
|
+
require 'open_feature/sdk'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module OpenFeature
|
|
8
|
+
# OpenFeature feature flagging provider backed by Datadog Remote Configuration.
|
|
9
|
+
#
|
|
10
|
+
# Implementation follows the OpenFeature contract of Provider SDK.
|
|
11
|
+
# For details see:
|
|
12
|
+
# - https://github.com/open-feature/ruby-sdk/blob/v0.4.1/README.md#develop-a-provider
|
|
13
|
+
# - https://github.com/open-feature/ruby-sdk/blob/v0.4.1/lib/open_feature/sdk/provider/no_op_provider.rb
|
|
14
|
+
#
|
|
15
|
+
# In the example below you can see how to configure the OpenFeature SDK
|
|
16
|
+
# https://github.com/open-feature/ruby-sdk to use the Datadog feature flags provider.
|
|
17
|
+
#
|
|
18
|
+
# Example:
|
|
19
|
+
#
|
|
20
|
+
# Make sure to enable Remote Configuration and OpenFeature in the Datadog configuration.
|
|
21
|
+
#
|
|
22
|
+
# ```ruby
|
|
23
|
+
# # FILE: initializers/datadog.rb
|
|
24
|
+
# Datadog.configure do |config|
|
|
25
|
+
# config.remote.enabled = true
|
|
26
|
+
# config.open_feature.enabled = true
|
|
27
|
+
# end
|
|
28
|
+
# ```
|
|
29
|
+
#
|
|
30
|
+
# And configure the OpenFeature SDK to use the Datadog feature flagging provider.
|
|
31
|
+
#
|
|
32
|
+
# ```ruby
|
|
33
|
+
# # FILE: initializers/open_feature.rb
|
|
34
|
+
# require 'open_feature/sdk'
|
|
35
|
+
# require 'datadog/open_feature/provider'
|
|
36
|
+
#
|
|
37
|
+
# OpenFeature::SDK.configure do |config|
|
|
38
|
+
# config.set_provider(Datadog::OpenFeature::Provider.new)
|
|
39
|
+
# end
|
|
40
|
+
# ```
|
|
41
|
+
#
|
|
42
|
+
# Now you can create OpenFeature SDK client and use it to fetch feature flag values.
|
|
43
|
+
#
|
|
44
|
+
# ```ruby
|
|
45
|
+
# client = OpenFeature::SDK.build_client
|
|
46
|
+
# context = OpenFeature::SDK::EvaluationContext.new('email' => 'john.doe@gmail.com')
|
|
47
|
+
#
|
|
48
|
+
# client.fetch_string_value(
|
|
49
|
+
# flag_key: 'banner', default_value: 'Greetings!', evaluation_context: context
|
|
50
|
+
# )
|
|
51
|
+
# # => 'Welcome back!'
|
|
52
|
+
# ```
|
|
53
|
+
class Provider
|
|
54
|
+
NAME = 'Datadog Feature Flagging Provider'
|
|
55
|
+
|
|
56
|
+
attr_reader :metadata
|
|
57
|
+
|
|
58
|
+
def initialize
|
|
59
|
+
@metadata = ::OpenFeature::SDK::Provider::ProviderMetadata.new(name: NAME).freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def init
|
|
63
|
+
# no-op
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def shutdown
|
|
67
|
+
# no-op
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
71
|
+
evaluate(flag_key, default_value: default_value, expected_type: :boolean, evaluation_context: evaluation_context)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
75
|
+
evaluate(flag_key, default_value: default_value, expected_type: :string, evaluation_context: evaluation_context)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
79
|
+
evaluate(flag_key, default_value: default_value, expected_type: :number, evaluation_context: evaluation_context)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
83
|
+
evaluate(flag_key, default_value: default_value, expected_type: :integer, evaluation_context: evaluation_context)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
87
|
+
evaluate(flag_key, default_value: default_value, expected_type: :float, evaluation_context: evaluation_context)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
91
|
+
evaluate(flag_key, default_value: default_value, expected_type: :object, evaluation_context: evaluation_context)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def evaluate(flag_key, default_value:, expected_type:, evaluation_context:)
|
|
97
|
+
engine = OpenFeature.engine
|
|
98
|
+
return component_not_configured_default(default_value) if engine.nil?
|
|
99
|
+
|
|
100
|
+
result = engine.fetch_value(
|
|
101
|
+
flag_key,
|
|
102
|
+
default_value: default_value,
|
|
103
|
+
expected_type: expected_type,
|
|
104
|
+
evaluation_context: evaluation_context
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if result.error?
|
|
108
|
+
return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
109
|
+
value: default_value,
|
|
110
|
+
error_code: result.error_code,
|
|
111
|
+
error_message: result.error_message,
|
|
112
|
+
reason: result.reason
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
117
|
+
value: result.value,
|
|
118
|
+
variant: result.variant,
|
|
119
|
+
reason: result.reason,
|
|
120
|
+
flag_metadata: result.flag_metadata
|
|
121
|
+
)
|
|
122
|
+
rescue => e
|
|
123
|
+
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
124
|
+
value: default_value,
|
|
125
|
+
error_code: Ext::GENERAL,
|
|
126
|
+
error_message: "#{e.class}: #{e.message}",
|
|
127
|
+
reason: Ext::ERROR
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def component_not_configured_default(value)
|
|
132
|
+
::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
133
|
+
value: value,
|
|
134
|
+
error_code: Ext::PROVIDER_FATAL,
|
|
135
|
+
error_message: "Datadog's OpenFeature component must be configured",
|
|
136
|
+
reason: Ext::ERROR
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../core/remote/dispatcher'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
# This module contains the remote configuration functionality for OpenFeature
|
|
8
|
+
module Remote
|
|
9
|
+
ReadError = Class.new(StandardError)
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
FFE_FLAG_CONFIGURATION_RULES = 1 << 46
|
|
13
|
+
FFE_PRODUCTS = ['FFE_FLAGS'].freeze
|
|
14
|
+
FFE_CAPABILITIES = [FFE_FLAG_CONFIGURATION_RULES].freeze
|
|
15
|
+
|
|
16
|
+
def capabilities
|
|
17
|
+
FFE_CAPABILITIES
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def products
|
|
21
|
+
FFE_PRODUCTS
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def receivers(telemetry)
|
|
25
|
+
matcher = Core::Remote::Dispatcher::Matcher::Product.new(FFE_PRODUCTS)
|
|
26
|
+
receiver = Core::Remote::Dispatcher::Receiver.new(matcher) do |repository, changes|
|
|
27
|
+
engine = OpenFeature.engine
|
|
28
|
+
next unless engine
|
|
29
|
+
|
|
30
|
+
changes.each do |change|
|
|
31
|
+
content = repository[change.path]
|
|
32
|
+
|
|
33
|
+
unless content || change.type == :delete
|
|
34
|
+
next telemetry.error("OpenFeature: Remote Configuration change is not present on #{change.type}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# NOTE: In the current RC implementation we immediately apply the configuration,
|
|
38
|
+
# but that might change if we need to apply patches instead.
|
|
39
|
+
case change.type
|
|
40
|
+
when :insert, :update
|
|
41
|
+
begin
|
|
42
|
+
# @type var content: Core::Remote::Configuration::Content
|
|
43
|
+
engine.reconfigure!(read_content(content))
|
|
44
|
+
content.applied
|
|
45
|
+
rescue ReadError => e
|
|
46
|
+
content.errored("Error reading Remote Configuration content: #{e.message}")
|
|
47
|
+
rescue EvaluationEngine::ReconfigurationError => e
|
|
48
|
+
content.errored("Error applying OpenFeature configuration: #{e.message}")
|
|
49
|
+
end
|
|
50
|
+
when :delete
|
|
51
|
+
# NOTE: For now, we treat deletion as clearing the configuration
|
|
52
|
+
# In a multi-config scenario, we might track configs per path
|
|
53
|
+
engine.reconfigure!(nil)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[receiver]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def read_content(content)
|
|
64
|
+
data = content.data.read
|
|
65
|
+
content.data.rewind
|
|
66
|
+
|
|
67
|
+
raise ReadError, 'EOF reached' if data.nil?
|
|
68
|
+
|
|
69
|
+
data
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ext'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
# This class is based on the `OpenFeature::SDK::Provider::ResolutionDetails` class
|
|
8
|
+
#
|
|
9
|
+
# See: https://github.com/open-feature/ruby-sdk/blob/v0.4.1/lib/open_feature/sdk/provider/resolution_details.rb
|
|
10
|
+
class ResolutionDetails < Struct.new(
|
|
11
|
+
:value,
|
|
12
|
+
:reason,
|
|
13
|
+
:variant,
|
|
14
|
+
:error_code,
|
|
15
|
+
:error_message,
|
|
16
|
+
:flag_metadata,
|
|
17
|
+
:allocation_key,
|
|
18
|
+
:extra_logging,
|
|
19
|
+
:log?,
|
|
20
|
+
:error?,
|
|
21
|
+
keyword_init: true
|
|
22
|
+
)
|
|
23
|
+
def self.build_error(value:, error_code:, error_message:, reason: Ext::ERROR)
|
|
24
|
+
new(
|
|
25
|
+
value: value,
|
|
26
|
+
error_code: error_code,
|
|
27
|
+
error_message: error_message,
|
|
28
|
+
reason: reason,
|
|
29
|
+
error?: true,
|
|
30
|
+
log?: false
|
|
31
|
+
).freeze
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../core/transport/http'
|
|
4
|
+
require_relative '../core/transport/http/env'
|
|
5
|
+
require_relative '../core/transport/parcel'
|
|
6
|
+
require_relative '../core/transport/request'
|
|
7
|
+
|
|
8
|
+
module Datadog
|
|
9
|
+
module OpenFeature
|
|
10
|
+
module Transport
|
|
11
|
+
class EncodedParcel
|
|
12
|
+
include Core::Transport::Parcel
|
|
13
|
+
|
|
14
|
+
def encode_with(encoder)
|
|
15
|
+
encoder.encode(data)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class HTTP
|
|
20
|
+
class Spec < Core::Transport::HTTP::API::Spec
|
|
21
|
+
def initialize
|
|
22
|
+
@endpoint = Core::Transport::HTTP::API::Endpoint.new(
|
|
23
|
+
:post, '/evp_proxy/v2/api/v2/exposures'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def call(env, &block)
|
|
30
|
+
@endpoint.call(env) do |request_env|
|
|
31
|
+
request_env.headers['Content-Type'] = Core::Encoding::JSONEncoder.content_type
|
|
32
|
+
request_env.headers['X-Datadog-EVP-Subdomain'] = 'event-platform-intake'
|
|
33
|
+
request_env.body = env.request.parcel.encode_with(Core::Encoding::JSONEncoder)
|
|
34
|
+
|
|
35
|
+
block.call(request_env)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Instance < Core::Transport::HTTP::API::Instance
|
|
41
|
+
def send_exposures(env)
|
|
42
|
+
@spec.call(env) { |request_env| call(request_env) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.build(agent_settings:, logger:)
|
|
47
|
+
Core::Transport::HTTP.build(
|
|
48
|
+
api_instance_class: HTTP::Instance,
|
|
49
|
+
agent_settings: agent_settings,
|
|
50
|
+
logger: logger
|
|
51
|
+
) { |t| t.api('exposures', HTTP::Spec.new) }.to_transport(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(apis, default_api, logger:)
|
|
55
|
+
@api = apis[default_api]
|
|
56
|
+
@logger = logger
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def send_exposures(payload)
|
|
60
|
+
request = Core::Transport::Request.new(EncodedParcel.new(payload))
|
|
61
|
+
@api.send_exposures(Core::Transport::HTTP::Env.new(request))
|
|
62
|
+
rescue => e
|
|
63
|
+
message = "Internal error during request. Cause: #{e.class.name} #{e.message} " \
|
|
64
|
+
"Location: #{Array(e.backtrace).first}"
|
|
65
|
+
@logger.debug(message)
|
|
66
|
+
|
|
67
|
+
Core::Transport::InternalErrorResponse.new(e)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'core/configuration'
|
|
4
|
+
require_relative 'open_feature/configuration'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
# A namespace for the OpenFeature component.
|
|
8
|
+
module OpenFeature
|
|
9
|
+
Core::Configuration::Settings.extend(Configuration::Settings)
|
|
10
|
+
|
|
11
|
+
def self.enabled?
|
|
12
|
+
Datadog.configuration.open_feature.enabled
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.engine
|
|
16
|
+
Datadog.send(:components).open_feature&.engine
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|