cw-datadog 2.23.0.2 → 2.23.0.3
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/ext/datadog_profiling_native_extension/extconf.rb +4 -2
- 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 +20 -5
- data/lib/datadog/appsec/api_security/sampler.rb +3 -1
- 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/cloudwise/client.rb +364 -25
- data/lib/datadog/core/cloudwise/component.rb +197 -52
- data/lib/datadog/core/cloudwise/docc_heartbeat_worker.rb +105 -0
- data/lib/datadog/core/cloudwise/docc_operation_worker.rb +191 -0
- data/lib/datadog/core/cloudwise/docc_registration_worker.rb +89 -0
- data/lib/datadog/core/cloudwise/license_worker.rb +3 -1
- data/lib/datadog/core/cloudwise/probe_state.rb +134 -12
- data/lib/datadog/core/configuration/components.rb +10 -9
- data/lib/datadog/core/configuration/settings.rb +28 -0
- data/lib/datadog/core/configuration/supported_configurations.rb +5 -2
- data/lib/datadog/core/remote/client/capabilities.rb +7 -0
- data/lib/datadog/core/remote/component.rb +2 -2
- 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 +23 -35
- data/lib/datadog/core/telemetry/component.rb +26 -13
- data/lib/datadog/core/telemetry/event/app_started.rb +67 -49
- data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +27 -4
- 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/telemetry/worker.rb +51 -6
- data/lib/datadog/core/transport/http/adapters/net.rb +2 -0
- data/lib/datadog/core/transport/http/client.rb +69 -0
- data/lib/datadog/core/utils/only_once_successful.rb +6 -2
- data/lib/datadog/data_streams/transport/http/client.rb +4 -32
- data/lib/datadog/data_streams/transport/stats.rb +1 -1
- data/lib/datadog/di/probe_notification_builder.rb +35 -13
- 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/input.rb +2 -2
- 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 +59 -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 +13 -0
- data/lib/datadog/open_feature/noop_evaluator.rb +26 -0
- data/lib/datadog/open_feature/provider.rb +134 -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/profiling/component.rb +6 -0
- data/lib/datadog/profiling/profiler.rb +4 -0
- data/lib/datadog/profiling.rb +1 -2
- data/lib/datadog/single_step_instrument.rb +1 -1
- data/lib/datadog/tracing/contrib/cloudwise/propagation.rb +164 -7
- data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +22 -17
- data/lib/datadog/tracing/contrib/karafka/framework.rb +30 -0
- data/lib/datadog/tracing/contrib/karafka/patcher.rb +14 -0
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +6 -2
- 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/transport/http/api.rb +40 -1
- data/lib/datadog/tracing/transport/http/client.rb +12 -26
- data/lib/datadog/tracing/transport/http/traces.rb +4 -2
- data/lib/datadog/tracing/transport/trace_formatter.rb +16 -0
- data/lib/datadog/version.rb +2 -2
- data/lib/datadog.rb +1 -0
- metadata +38 -15
- data/lib/datadog/core/cloudwise/IMPLEMENTATION_V2.md +0 -517
- data/lib/datadog/core/cloudwise/QUICKSTART.md +0 -398
- data/lib/datadog/core/cloudwise/README.md +0 -722
- 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
|
@@ -1,47 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../../../core/transport/http/
|
|
3
|
+
require_relative '../../../core/transport/http/client'
|
|
4
4
|
|
|
5
5
|
module Datadog
|
|
6
6
|
module DataStreams
|
|
7
7
|
module Transport
|
|
8
8
|
module HTTP
|
|
9
9
|
# HTTP client for Data Streams Monitoring
|
|
10
|
-
class Client
|
|
11
|
-
attr_reader :api, :logger
|
|
12
|
-
|
|
13
|
-
def initialize(api, logger:)
|
|
14
|
-
@api = api
|
|
15
|
-
@logger = logger
|
|
16
|
-
end
|
|
17
|
-
|
|
10
|
+
class Client < Core::Transport::HTTP::Client
|
|
18
11
|
def send_stats_payload(request)
|
|
19
12
|
send_request(request) do |api, env|
|
|
20
|
-
api
|
|
13
|
+
# TODO how to make api have the derived type for steep?
|
|
14
|
+
api.send_stats(env) # steep:ignore
|
|
21
15
|
end
|
|
22
16
|
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def send_request(request, &block)
|
|
27
|
-
# Build request into env
|
|
28
|
-
env = build_env(request)
|
|
29
|
-
|
|
30
|
-
# Get response from API
|
|
31
|
-
yield(api, env)
|
|
32
|
-
rescue => e
|
|
33
|
-
message =
|
|
34
|
-
"Internal error during #{self.class.name} request. Cause: #{e.class}: #{e} " \
|
|
35
|
-
"Location: #{Array(e.backtrace).first}"
|
|
36
|
-
|
|
37
|
-
logger.debug(message)
|
|
38
|
-
|
|
39
|
-
Datadog::Core::Transport::InternalErrorResponse.new(e)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def build_env(request)
|
|
43
|
-
Datadog::Core::Transport::HTTP::Env.new(request)
|
|
44
|
-
end
|
|
45
17
|
end
|
|
46
18
|
end
|
|
47
19
|
end
|
|
@@ -34,7 +34,7 @@ module Datadog
|
|
|
34
34
|
@default_api = default_api
|
|
35
35
|
@current_api_id = default_api
|
|
36
36
|
|
|
37
|
-
@client = HTTP::Client.new(current_api, logger: @logger)
|
|
37
|
+
@client = DataStreams::Transport::HTTP::Client.new(current_api, logger: @logger)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def send_stats(payload)
|
|
@@ -116,7 +116,12 @@ module Datadog
|
|
|
116
116
|
location = if probe.line?
|
|
117
117
|
{
|
|
118
118
|
file: context.path,
|
|
119
|
-
|
|
119
|
+
# Line numbers are required to be strings by the
|
|
120
|
+
# system tests schema.
|
|
121
|
+
# Backend I think accepts them also as integers, but some
|
|
122
|
+
# other languages send strings and we decided to require
|
|
123
|
+
# strings for everyone.
|
|
124
|
+
lines: [probe.line_no.to_s],
|
|
120
125
|
}
|
|
121
126
|
elsif probe.method?
|
|
122
127
|
{
|
|
@@ -131,19 +136,36 @@ module Datadog
|
|
|
131
136
|
|
|
132
137
|
{
|
|
133
138
|
service: settings.service,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
debugger: {
|
|
140
|
+
type: 'snapshot',
|
|
141
|
+
# Product can have three values: di, ld, er.
|
|
142
|
+
# We do not currently implement exception replay.
|
|
143
|
+
# There is currently no specification, and no consensus, for
|
|
144
|
+
# when product should be di (dynamic instrumentation) and when
|
|
145
|
+
# it should be ld (live debugger). I thought the backend was
|
|
146
|
+
# supposed to provide this in probe specification via remote
|
|
147
|
+
# config, but apparently this is not the case and the expectation
|
|
148
|
+
# is that the library figures out the product via heuristics,
|
|
149
|
+
# except there is currently no consensus on said heuristics.
|
|
150
|
+
# .NET always sends ld, other languages send nothing at the moment.
|
|
151
|
+
# Don't send anything for the time being.
|
|
152
|
+
#product: 'di/ld',
|
|
153
|
+
snapshot: {
|
|
154
|
+
id: SecureRandom.uuid,
|
|
155
|
+
timestamp: timestamp,
|
|
156
|
+
evaluationErrors: evaluation_errors,
|
|
157
|
+
probe: {
|
|
158
|
+
id: probe.id,
|
|
159
|
+
version: 0,
|
|
160
|
+
location: location,
|
|
161
|
+
},
|
|
162
|
+
language: 'ruby',
|
|
163
|
+
# TODO add test coverage for callers being nil
|
|
164
|
+
stack: stack,
|
|
165
|
+
# System tests schema validation requires captures to
|
|
166
|
+
# always be present
|
|
167
|
+
captures: captures || {},
|
|
142
168
|
},
|
|
143
|
-
language: 'ruby',
|
|
144
|
-
# TODO add test coverage for callers being nil
|
|
145
|
-
stack: stack,
|
|
146
|
-
captures: captures,
|
|
147
169
|
},
|
|
148
170
|
# In python tracer duration is under debugger.snapshot,
|
|
149
171
|
# but UI appears to expect it here at top level.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../../core/transport/parcel'
|
|
4
|
-
require_relative 'http/
|
|
4
|
+
require_relative 'http/diagnostics'
|
|
5
5
|
|
|
6
6
|
module Datadog
|
|
7
7
|
module DI
|
|
@@ -21,7 +21,7 @@ module Datadog
|
|
|
21
21
|
@apis = apis
|
|
22
22
|
@logger = logger
|
|
23
23
|
|
|
24
|
-
@client = HTTP::Client.new(current_api, logger: logger)
|
|
24
|
+
@client = DI::Transport::HTTP::Diagnostics::Client.new(current_api, logger: logger)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def current_api
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../../../core/transport/http/api/instance'
|
|
4
4
|
require_relative '../../../core/transport/http/api/spec'
|
|
5
|
-
require_relative 'client'
|
|
5
|
+
require_relative '../../../core/transport/http/client'
|
|
6
6
|
|
|
7
7
|
module Datadog
|
|
8
8
|
module DI
|
|
9
9
|
module Transport
|
|
10
10
|
module HTTP
|
|
11
11
|
module Diagnostics
|
|
12
|
-
|
|
12
|
+
class Client < Core::Transport::HTTP::Client
|
|
13
13
|
def send_diagnostics_payload(request)
|
|
14
14
|
send_request(request) do |api, env|
|
|
15
15
|
api.send_diagnostics(env)
|
|
@@ -57,8 +57,6 @@ module Datadog
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
|
-
|
|
61
|
-
HTTP::Client.include(Diagnostics::Client)
|
|
62
60
|
end
|
|
63
61
|
end
|
|
64
62
|
end
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative '../../../core/transport/http/api/instance'
|
|
4
4
|
require_relative '../../../core/transport/http/api/spec'
|
|
5
|
-
require_relative 'client'
|
|
5
|
+
require_relative '../../../core/transport/http/client'
|
|
6
6
|
|
|
7
7
|
module Datadog
|
|
8
8
|
module DI
|
|
9
9
|
module Transport
|
|
10
10
|
module HTTP
|
|
11
11
|
module Input
|
|
12
|
-
|
|
12
|
+
class Client < Core::Transport::HTTP::Client
|
|
13
13
|
def send_input_payload(request)
|
|
14
14
|
send_request(request) do |api, env|
|
|
15
15
|
api.send_input(env)
|
|
@@ -69,8 +69,6 @@ module Datadog
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
|
-
|
|
73
|
-
HTTP::Client.include(Input::Client)
|
|
74
72
|
end
|
|
75
73
|
end
|
|
76
74
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../../core/transport/parcel'
|
|
4
|
-
require_relative 'http/
|
|
4
|
+
require_relative 'http/input'
|
|
5
5
|
|
|
6
6
|
module Datadog
|
|
7
7
|
module DI
|
|
@@ -28,7 +28,7 @@ module Datadog
|
|
|
28
28
|
@apis = apis
|
|
29
29
|
@logger = logger
|
|
30
30
|
|
|
31
|
-
@client = HTTP::Client.new(current_api, logger: logger)
|
|
31
|
+
@client = DI::Transport::HTTP::Input::Client.new(current_api, logger: logger)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def current_api
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'transport'
|
|
4
|
+
require_relative 'evaluation_engine'
|
|
5
|
+
require_relative 'exposures/buffer'
|
|
6
|
+
require_relative 'exposures/worker'
|
|
7
|
+
require_relative 'exposures/deduplicator'
|
|
8
|
+
require_relative 'exposures/reporter'
|
|
9
|
+
|
|
10
|
+
module Datadog
|
|
11
|
+
module OpenFeature
|
|
12
|
+
# This class is the entry point for the OpenFeature component
|
|
13
|
+
class Component
|
|
14
|
+
attr_reader :engine
|
|
15
|
+
|
|
16
|
+
def self.build(settings, agent_settings, logger:, telemetry:)
|
|
17
|
+
return unless settings.respond_to?(:open_feature) && settings.open_feature.enabled
|
|
18
|
+
|
|
19
|
+
unless settings.respond_to?(:remote) && settings.remote.enabled
|
|
20
|
+
message = 'OpenFeature could not be enabled as Remote Configuration is currently disabled. ' \
|
|
21
|
+
'To enable Remote Configuration, see https://docs.datadoghq.com/remote_configuration/.'
|
|
22
|
+
|
|
23
|
+
logger.warn(message)
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if RUBY_ENGINE != 'ruby'
|
|
28
|
+
message = 'OpenFeature could not be enabled as MRI is required, ' \
|
|
29
|
+
"but running on #{RUBY_ENGINE.inspect}"
|
|
30
|
+
|
|
31
|
+
logger.warn(message)
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if (libdatadog_api_failure = Core::LIBDATADOG_API_FAILURE)
|
|
36
|
+
message = 'OpenFeature could not be enabled as `libdatadog` is not loaded: ' \
|
|
37
|
+
"#{libdatadog_api_failure.inspect}. For help solving this issue, " \
|
|
38
|
+
'please contact Datadog support at https://docs.datadoghq.com/help/.'
|
|
39
|
+
|
|
40
|
+
logger.warn(message)
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
new(settings, agent_settings, logger: logger, telemetry: telemetry)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(settings, agent_settings, logger:, telemetry:)
|
|
48
|
+
transport = Transport::HTTP.build(agent_settings: agent_settings, logger: logger)
|
|
49
|
+
@worker = Exposures::Worker.new(settings: settings, transport: transport, telemetry: telemetry, logger: logger)
|
|
50
|
+
|
|
51
|
+
reporter = Exposures::Reporter.new(@worker, telemetry: telemetry, logger: logger)
|
|
52
|
+
@engine = EvaluationEngine.new(reporter, telemetry: telemetry, logger: logger)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def shutdown!
|
|
56
|
+
@worker.graceful_shutdown
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module OpenFeature
|
|
5
|
+
module Configuration
|
|
6
|
+
# A settings class for the OpenFeature component.
|
|
7
|
+
module Settings
|
|
8
|
+
def self.extended(base)
|
|
9
|
+
base = base.singleton_class unless base.is_a?(Class)
|
|
10
|
+
add_settings!(base)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.add_settings!(base)
|
|
14
|
+
base.class_eval do
|
|
15
|
+
settings :open_feature do
|
|
16
|
+
option :enabled do |o|
|
|
17
|
+
o.type :bool
|
|
18
|
+
o.env 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'
|
|
19
|
+
o.default false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ext'
|
|
4
|
+
require_relative 'noop_evaluator'
|
|
5
|
+
require_relative 'resolution_details'
|
|
6
|
+
|
|
7
|
+
module Datadog
|
|
8
|
+
module OpenFeature
|
|
9
|
+
# This class performs the evaluation of the feature flag
|
|
10
|
+
class EvaluationEngine
|
|
11
|
+
ReconfigurationError = Class.new(StandardError)
|
|
12
|
+
|
|
13
|
+
ALLOWED_TYPES = %w[boolean string number float integer object].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(reporter, telemetry:, logger:)
|
|
16
|
+
@reporter = reporter
|
|
17
|
+
@telemetry = telemetry
|
|
18
|
+
@logger = logger
|
|
19
|
+
|
|
20
|
+
@evaluator = NoopEvaluator.new(nil)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch_value(flag_key:, default_value:, expected_type:, evaluation_context: nil)
|
|
24
|
+
unless ALLOWED_TYPES.include?(expected_type)
|
|
25
|
+
message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
|
|
26
|
+
return ResolutionDetails.build_error(
|
|
27
|
+
value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context = evaluation_context&.fields || {}
|
|
32
|
+
result = @evaluator.get_assignment(flag_key, default_value, context, expected_type)
|
|
33
|
+
|
|
34
|
+
@reporter.report(result, flag_key: flag_key, context: evaluation_context)
|
|
35
|
+
|
|
36
|
+
result
|
|
37
|
+
rescue => e
|
|
38
|
+
@telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
|
|
39
|
+
|
|
40
|
+
ResolutionDetails.build_error(
|
|
41
|
+
value: default_value, error_code: Ext::PROVIDER_FATAL, error_message: e.message
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reconfigure!(configuration)
|
|
46
|
+
@logger.debug('OpenFeature: Removing configuration') if configuration.nil?
|
|
47
|
+
|
|
48
|
+
@evaluator = NoopEvaluator.new(configuration)
|
|
49
|
+
rescue => e
|
|
50
|
+
message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
|
|
51
|
+
|
|
52
|
+
@logger.error("#{message}, error #{e.inspect}")
|
|
53
|
+
@telemetry.report(e, description: message)
|
|
54
|
+
|
|
55
|
+
raise ReconfigurationError, e.message
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module OpenFeature
|
|
5
|
+
module Exposures
|
|
6
|
+
# This class builds a batch of exposures and context to be sent to the Agent
|
|
7
|
+
class BatchBuilder
|
|
8
|
+
def initialize(settings)
|
|
9
|
+
@context = build_context(settings)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def payload_for(events)
|
|
13
|
+
{
|
|
14
|
+
context: @context,
|
|
15
|
+
exposures: events
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_context(settings)
|
|
22
|
+
context = {}
|
|
23
|
+
context[:env] = settings.env if settings.env
|
|
24
|
+
context[:service] = settings.service if settings.service
|
|
25
|
+
context[:version] = settings.version if settings.version
|
|
26
|
+
|
|
27
|
+
context
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/buffer/cruby'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
module Exposures
|
|
8
|
+
# This class is a buffer for exposure events that evicts at random and
|
|
9
|
+
# keeps track of the number of dropped events
|
|
10
|
+
#
|
|
11
|
+
# WARNING: This class does not work as intended on JRuby
|
|
12
|
+
class Buffer < Core::Buffer::CRuby
|
|
13
|
+
DEFAULT_LIMIT = 1_000
|
|
14
|
+
|
|
15
|
+
attr_reader :dropped_count
|
|
16
|
+
|
|
17
|
+
def initialize(limit = DEFAULT_LIMIT)
|
|
18
|
+
@dropped = 0
|
|
19
|
+
@dropped_count = 0
|
|
20
|
+
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def drain!
|
|
27
|
+
drained = super
|
|
28
|
+
|
|
29
|
+
@dropped_count = @dropped
|
|
30
|
+
@dropped = 0
|
|
31
|
+
|
|
32
|
+
drained
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def replace!(item)
|
|
36
|
+
@dropped += 1
|
|
37
|
+
|
|
38
|
+
super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/utils/lru_cache'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
module Exposures
|
|
8
|
+
# This class is a deduplication buffer based on LRU cache for exposure events
|
|
9
|
+
class Deduplicator
|
|
10
|
+
DEFAULT_CACHE_LIMIT = 1_000
|
|
11
|
+
|
|
12
|
+
def initialize(limit: DEFAULT_CACHE_LIMIT)
|
|
13
|
+
@cache = Datadog::Core::Utils::LRUCache.new(limit)
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def duplicate?(key, value)
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
stored = @cache[key]
|
|
20
|
+
return true if stored == value
|
|
21
|
+
|
|
22
|
+
@cache[key] = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/utils/time'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module OpenFeature
|
|
7
|
+
module Exposures
|
|
8
|
+
# A data model for an exposure event
|
|
9
|
+
module Event
|
|
10
|
+
TARGETING_KEY_FIELD = 'targeting_key'
|
|
11
|
+
ALLOWED_FIELD_TYPES = [String, Integer, Float, TrueClass, FalseClass].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def cache_key(result, flag_key:, context:)
|
|
15
|
+
"#{flag_key}:#{context.targeting_key}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def cache_value(result, flag_key:, context:)
|
|
19
|
+
"#{result.allocation_key}:#{result.variant}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build(result, flag_key:, context:)
|
|
23
|
+
{
|
|
24
|
+
timestamp: current_timestamp_ms,
|
|
25
|
+
allocation: {
|
|
26
|
+
key: result.allocation_key
|
|
27
|
+
},
|
|
28
|
+
flag: {
|
|
29
|
+
key: flag_key
|
|
30
|
+
},
|
|
31
|
+
variant: {
|
|
32
|
+
key: result.variant
|
|
33
|
+
},
|
|
34
|
+
subject: {
|
|
35
|
+
id: context.targeting_key,
|
|
36
|
+
attributes: extract_attributes(context)
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# NOTE: We take all filds of the context that does not support nesting
|
|
44
|
+
# and will ignore targeting key as it will be set as `subject.id`
|
|
45
|
+
def extract_attributes(context)
|
|
46
|
+
context.fields.select do |key, value|
|
|
47
|
+
next false if key == TARGETING_KEY_FIELD
|
|
48
|
+
|
|
49
|
+
ALLOWED_FIELD_TYPES.include?(value.class)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def current_timestamp_ms
|
|
54
|
+
(Datadog::Core::Utils::Time.now.to_f * 1000).to_i
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -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
|