cw-datadog 2.23.0.4 → 2.23.0.5
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/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/lib/datadog/core/cloudwise/client.rb +99 -4
- data/lib/datadog/core/cloudwise/component.rb +55 -13
- data/lib/datadog/core/cloudwise/docc_operation_worker.rb +11 -1
- data/lib/datadog/core/cloudwise/license_worker.rb +7 -0
- data/lib/datadog/core/cloudwise/time_sync_worker.rb +200 -0
- data/lib/datadog/core/configuration/settings.rb +12 -0
- data/lib/datadog/core/configuration/supported_configurations.rb +14 -0
- 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/tag_normalizer.rb +84 -0
- data/lib/datadog/core/transport/http/adapters/net.rb +8 -0
- data/lib/datadog/core/utils/array.rb +29 -0
- data/lib/datadog/core/utils.rb +2 -0
- data/lib/datadog/data_streams/processor.rb +1 -1
- data/lib/datadog/di/transport/http.rb +6 -2
- data/lib/datadog/di/transport/input.rb +62 -2
- data/lib/datadog/open_feature/evaluation_engine.rb +19 -9
- data/lib/datadog/open_feature/ext.rb +1 -0
- data/lib/datadog/open_feature/native_evaluator.rb +38 -0
- data/lib/datadog/open_feature/noop_evaluator.rb +3 -3
- data/lib/datadog/open_feature/provider.rb +15 -8
- data/lib/datadog/open_feature/remote.rb +1 -1
- 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/tracing/configuration/ext.rb +1 -0
- data/lib/datadog/tracing/contrib/cloudwise/propagation.rb +143 -17
- data/lib/datadog/tracing/contrib/grape/endpoint.rb +141 -0
- data/lib/datadog/tracing/contrib/kafka/events/consumer/process_batch.rb +26 -0
- data/lib/datadog/tracing/contrib/kafka/events/consumer/process_message.rb +26 -0
- data/lib/datadog/tracing/contrib/kafka/instrumentation/consumer.rb +79 -9
- data/lib/datadog/tracing/contrib/kafka/instrumentation/producer.rb +29 -6
- data/lib/datadog/tracing/contrib/rack/middlewares.rb +6 -54
- data/lib/datadog/tracing/diagnostics/environment_logger.rb +1 -1
- data/lib/datadog/tracing/transport/serializable_trace.rb +8 -1
- data/lib/datadog/tracing/transport/trace_formatter.rb +11 -0
- data/lib/datadog/tracing/transport/traces.rb +3 -5
- data/lib/datadog/version.rb +1 -1
- metadata +29 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'ext'
|
|
4
|
+
require_relative '../tag_normalizer'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module Core
|
|
8
|
+
module Environment
|
|
9
|
+
# Retrieves process level information such that it can be attached to various payloads
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Process
|
|
13
|
+
# This method returns a key/value part of serialized tags in the format of k1:v1,k2:v2,k3:v3
|
|
14
|
+
# @return [String] comma-separated normalized key:value pairs
|
|
15
|
+
def self.serialized
|
|
16
|
+
return @serialized if defined?(@serialized)
|
|
17
|
+
tags = []
|
|
18
|
+
|
|
19
|
+
workdir = TagNormalizer.normalize_process_value(entrypoint_workdir.to_s)
|
|
20
|
+
tags << "#{Environment::Ext::TAG_ENTRYPOINT_WORKDIR}:#{workdir}" unless workdir.empty?
|
|
21
|
+
|
|
22
|
+
entry_name = TagNormalizer.normalize_process_value(entrypoint_name.to_s)
|
|
23
|
+
tags << "#{Environment::Ext::TAG_ENTRYPOINT_NAME}:#{entry_name}" unless entry_name.empty?
|
|
24
|
+
|
|
25
|
+
basedir = TagNormalizer.normalize_process_value(entrypoint_basedir.to_s)
|
|
26
|
+
tags << "#{Environment::Ext::TAG_ENTRYPOINT_BASEDIR}:#{basedir}" unless basedir.empty?
|
|
27
|
+
|
|
28
|
+
tags << "#{Environment::Ext::TAG_ENTRYPOINT_TYPE}:#{TagNormalizer.normalize(entrypoint_type, remove_digit_start_char: false)}"
|
|
29
|
+
|
|
30
|
+
@serialized = tags.join(',').freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the last segment of the working directory of the process
|
|
34
|
+
# Example: /app/myapp -> myapp
|
|
35
|
+
# @return [String] the last segment of the working directory
|
|
36
|
+
def self.entrypoint_workdir
|
|
37
|
+
File.basename(Dir.pwd)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the entrypoint type of the process
|
|
41
|
+
# In Ruby, the entrypoint type is always 'script'
|
|
42
|
+
# @return [String] the type of the process, which is fixed in Ruby
|
|
43
|
+
def self.entrypoint_type
|
|
44
|
+
Environment::Ext::PROCESS_TYPE
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the last segment of the base directory of the process
|
|
48
|
+
# Example 1: /bin/mybin -> mybin
|
|
49
|
+
# Example 2: ruby /test/myapp.rb -> myapp
|
|
50
|
+
# @return [String] the last segment of base directory of the script
|
|
51
|
+
#
|
|
52
|
+
# @note Determining true entrypoint name is rather complicated. This method
|
|
53
|
+
# is the initial implementation but it does not produce optimal output in all cases.
|
|
54
|
+
# For example, all Rails applications launched via `rails server` get `rails`
|
|
55
|
+
# as their entrypoint name.
|
|
56
|
+
# We might improve the behavior in the future if there is customer demand for it.
|
|
57
|
+
def self.entrypoint_name
|
|
58
|
+
File.basename($0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the last segment of the base directory of the process
|
|
62
|
+
# Example 1: /bin/mybin -> bin
|
|
63
|
+
# Example 2: ruby /test/myapp.js -> test
|
|
64
|
+
# @return [String] the last segment of the base directory of the script
|
|
65
|
+
#
|
|
66
|
+
# @note As with entrypoint name, determining true entrypoint directory is complicated.
|
|
67
|
+
# This method has an initial implementation that does not necessarily return good
|
|
68
|
+
# results in all cases. For example, for Rails applications launched via `rails server`
|
|
69
|
+
# the entrypoint basedir is `bin` which is not very helpful.
|
|
70
|
+
# We might improve this in the future if there is customer demand.
|
|
71
|
+
def self.entrypoint_basedir
|
|
72
|
+
File.basename(File.expand_path(File.dirname($0)))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private_class_method :entrypoint_workdir, :entrypoint_type, :entrypoint_name, :entrypoint_basedir
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module Core
|
|
7
|
+
# Feature flags evaluation using libdatadog
|
|
8
|
+
# The classes in this module are defined as C extensions in ext/libdatadog_api/feature_flags.c
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module FeatureFlags
|
|
12
|
+
# A top-level error raised by the extension
|
|
13
|
+
class Error < StandardError # rubocop:disable Lint/EmptyClass
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Configuration for feature flags evaluation
|
|
17
|
+
# This class is defined in the C extension
|
|
18
|
+
class Configuration # rubocop:disable Lint/EmptyClass
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Resolution details for a feature flag evaluation
|
|
22
|
+
# Base class is defined in the C extension, with Ruby methods added here
|
|
23
|
+
class ResolutionDetails
|
|
24
|
+
attr_writer :value
|
|
25
|
+
|
|
26
|
+
# Get the resolved value, with JSON parsing for object types
|
|
27
|
+
#
|
|
28
|
+
# @return [Object] The resolved value (parsed from JSON if object type)
|
|
29
|
+
# @raise [Datadog::Core::FeatureFlags::Error] If JSON parsing fails
|
|
30
|
+
def value
|
|
31
|
+
return @value if defined?(@value)
|
|
32
|
+
|
|
33
|
+
# NOTE: Raw value method call doesn't support memoization right now
|
|
34
|
+
value = raw_value
|
|
35
|
+
|
|
36
|
+
# NOTE: Lazy parsing of the JSON is a temporary solution and will be
|
|
37
|
+
# moved into C extension
|
|
38
|
+
@value = json?(value) ? JSON.parse(value) : value
|
|
39
|
+
rescue JSON::ParserError => e
|
|
40
|
+
raise Error, "Failed to parse JSON value: #{e.class}: #{e}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if the resolution resulted in an error
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] True if there was an error
|
|
46
|
+
def error?
|
|
47
|
+
reason == 'ERROR'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
# NOTE: A JSON raw string will be returned by the `libdatadog` as
|
|
53
|
+
# a Ruby String class with a flag type `:object`, otherwise it's
|
|
54
|
+
# just a string.
|
|
55
|
+
def json?(value)
|
|
56
|
+
flag_type == :object && value.is_a?(String)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'utils'
|
|
4
|
+
|
|
5
|
+
module Datadog
|
|
6
|
+
module Core
|
|
7
|
+
# @api private
|
|
8
|
+
module TagNormalizer
|
|
9
|
+
# Normalization logic used for tag keys and values that the Trace Agent has for traces
|
|
10
|
+
# Useful for ensuring that tag keys and values are normalized consistently
|
|
11
|
+
# An use case for now is Process Tags which need to be sent across various intakes (profiling, tracing, etc.) consistently
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
INVALID_TAG_CHARACTERS = %r{[^\p{L}0-9_\-:./]}
|
|
16
|
+
LEADING_INVALID_CHARS_NO_DIGITS = %r{\A[^\p{L}:]++}
|
|
17
|
+
LEADING_INVALID_CHARS_WITH_DIGITS = %r{\A[^\p{L}0-9:./]++}
|
|
18
|
+
MAX_BYTE_SIZE = 200 # Represents the general max tag length
|
|
19
|
+
MAX_PROCESS_VALUE_BYTE_SIZE = 100 # Represents the max tag length for process tags
|
|
20
|
+
VALID_ASCII_TAG = %r{\A[a-z:][a-z0-9:./-]*\z}
|
|
21
|
+
|
|
22
|
+
# Based on https://github.com/DataDog/datadog-agent/blob/45799c842bbd216bcda208737f9f11cade6fdd95/pkg/trace/traceutil/normalize.go#L131
|
|
23
|
+
# Specifically for general normalization:
|
|
24
|
+
# - Must be valid UTF-8
|
|
25
|
+
# - Invalid characters are replaced with an underscore
|
|
26
|
+
# - Leading non-letter characters are removed but colons are kept
|
|
27
|
+
# - Trailing non-letter characters are removed
|
|
28
|
+
# - Trailing underscores are removed
|
|
29
|
+
# - Consecutive underscores are merged into a single underscore
|
|
30
|
+
# - Maximum length is 200 characters
|
|
31
|
+
# If it's a tag value, allow it to start with a digit
|
|
32
|
+
# @param original_value [String] The original string
|
|
33
|
+
# @param remove_digit_start_char [Boolean] - whether to remove the leading digit (currently only used for tag values)
|
|
34
|
+
# @return [String] The normalized string
|
|
35
|
+
def self.normalize(original_value, remove_digit_start_char: false)
|
|
36
|
+
# DEV-3.0: Ideally this encode call should be replaced with Datadog::Core::Utils.utf8_encode once it
|
|
37
|
+
# is safe to modify the default behavior.
|
|
38
|
+
value = original_value.to_s.encode('UTF-8', invalid: :replace, undef: :replace)
|
|
39
|
+
value.strip!
|
|
40
|
+
return "" if value.empty?
|
|
41
|
+
|
|
42
|
+
return value if value.bytesize <= MAX_BYTE_SIZE &&
|
|
43
|
+
value.match?(VALID_ASCII_TAG)
|
|
44
|
+
|
|
45
|
+
if value.bytesize > MAX_BYTE_SIZE
|
|
46
|
+
value = value.byteslice(0, MAX_BYTE_SIZE)
|
|
47
|
+
value.scrub!("")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
value.downcase!
|
|
51
|
+
value.gsub!(INVALID_TAG_CHARACTERS, '_')
|
|
52
|
+
|
|
53
|
+
# The Trace Agent allows tag values to start with a number so this logic is here too
|
|
54
|
+
leading_invalid_regex = remove_digit_start_char ? LEADING_INVALID_CHARS_NO_DIGITS : LEADING_INVALID_CHARS_WITH_DIGITS
|
|
55
|
+
value.sub!(leading_invalid_regex, "")
|
|
56
|
+
|
|
57
|
+
value.squeeze!('_') if value.include?('__')
|
|
58
|
+
value.delete_suffix!('_')
|
|
59
|
+
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Process tags values follow an additional piece of normalization:
|
|
64
|
+
# - must not be more than 100 bytes
|
|
65
|
+
# - and must not contain colons
|
|
66
|
+
# @param value [String] The original string
|
|
67
|
+
# @return [String] The normalized string
|
|
68
|
+
def self.normalize_process_value(value)
|
|
69
|
+
value = normalize(value)
|
|
70
|
+
return value if value.empty?
|
|
71
|
+
|
|
72
|
+
value.tr!(':', '_')
|
|
73
|
+
value.squeeze!('_') if value.include?('__')
|
|
74
|
+
|
|
75
|
+
if value.bytesize > MAX_PROCESS_VALUE_BYTE_SIZE
|
|
76
|
+
value = value.byteslice(0, MAX_PROCESS_VALUE_BYTE_SIZE) || value
|
|
77
|
+
value.scrub!("")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -81,6 +81,14 @@ module Datadog
|
|
|
81
81
|
http.request(post)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Debug log: response result
|
|
85
|
+
Datadog.logger.debug do
|
|
86
|
+
response_body = http_response.body.to_s
|
|
87
|
+
# 截断过长的响应体
|
|
88
|
+
truncated_body = response_body.length > 500 ? "#{response_body[0..500]}..." : response_body
|
|
89
|
+
"[Trace Transport] Flushing trace response: POST.uri =#{net_http_path_from_env(env)}, body=#{truncated_body}"
|
|
90
|
+
end if defined?(Datadog.logger)
|
|
91
|
+
|
|
84
92
|
# Build and return response
|
|
85
93
|
Response.new(http_response)
|
|
86
94
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module Core
|
|
5
|
+
module Utils
|
|
6
|
+
# Common array-related utility functions.
|
|
7
|
+
module Array
|
|
8
|
+
def self.filter_map(array, &block)
|
|
9
|
+
if array.respond_to?(:filter_map)
|
|
10
|
+
# DEV Supported since Ruby 2.7, saves an intermediate object creation
|
|
11
|
+
array.filter_map(&block)
|
|
12
|
+
elsif array.is_a?(Enumerator::Lazy)
|
|
13
|
+
# You would think that .compact would work here, but it does not:
|
|
14
|
+
# the result of .map could be an Enumerator::Lazy instance which
|
|
15
|
+
# does not implement #compact on Ruby 2.5/2.6.
|
|
16
|
+
array.map(&block).reject do |item|
|
|
17
|
+
item.nil?
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
array.each_with_object([]) do |item, memo|
|
|
21
|
+
new_item = block.call(item)
|
|
22
|
+
memo.push(new_item) unless new_item.nil?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/datadog/core/utils.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Datadog
|
|
|
38
38
|
|
|
39
39
|
# Ensure `str` is a valid UTF-8, ready to be
|
|
40
40
|
# sent through the tracer transport.
|
|
41
|
+
# DEV-3.0: This method should unconditionally handle invalid byte sequences
|
|
42
|
+
# DEV-3.0: and return a safe string to display.
|
|
41
43
|
#
|
|
42
44
|
# @param [String,#to_s] str object to be converted to a UTF-8 string
|
|
43
45
|
# @param [Boolean] binary whether to expect binary data in the `str` parameter
|
|
@@ -40,9 +40,13 @@ module Datadog
|
|
|
40
40
|
api_version: nil,
|
|
41
41
|
headers: nil
|
|
42
42
|
)
|
|
43
|
-
Core::Transport::HTTP.build(
|
|
43
|
+
Core::Transport::HTTP.build(
|
|
44
|
+
api_instance_class: Input::API::Instance,
|
|
44
45
|
logger: logger,
|
|
45
|
-
agent_settings: agent_settings,
|
|
46
|
+
agent_settings: agent_settings,
|
|
47
|
+
api_version: api_version,
|
|
48
|
+
headers: headers,
|
|
49
|
+
) do |transport|
|
|
46
50
|
apis = API.defaults
|
|
47
51
|
|
|
48
52
|
transport.api API::INPUT, apis[API::INPUT]
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../../core/chunker'
|
|
4
|
+
require_relative '../../core/encoding'
|
|
5
|
+
require_relative '../../core/tag_builder'
|
|
3
6
|
require_relative '../../core/transport/parcel'
|
|
7
|
+
require_relative '../../core/transport/request'
|
|
8
|
+
require_relative '../error'
|
|
4
9
|
require_relative 'http/input'
|
|
5
10
|
|
|
6
11
|
module Datadog
|
|
@@ -24,6 +29,25 @@ module Datadog
|
|
|
24
29
|
class Transport
|
|
25
30
|
attr_reader :client, :apis, :default_api, :current_api_id, :logger
|
|
26
31
|
|
|
32
|
+
# The limit on an individual snapshot payload, aka "log line",
|
|
33
|
+
# is 1 MB.
|
|
34
|
+
#
|
|
35
|
+
# TODO There is an RFC for snapshot pruning that should be
|
|
36
|
+
# implemented to reduce the size of snapshots to be below this
|
|
37
|
+
# limit, so that we can send a portion of the captured data
|
|
38
|
+
# rather than dropping the snapshot entirely.
|
|
39
|
+
MAX_SERIALIZED_SNAPSHOT_SIZE = 1024 * 1024
|
|
40
|
+
|
|
41
|
+
# The maximum chunk (batch) size that intake permits is 5 MB.
|
|
42
|
+
#
|
|
43
|
+
# Two bytes are for the [ and ] of JSON array syntax.
|
|
44
|
+
MAX_CHUNK_SIZE = 5 * 1024 * 1024 - 2
|
|
45
|
+
|
|
46
|
+
# Try to send smaller payloads to avoid large network requests.
|
|
47
|
+
# If a payload is larger than default chunk size but is under the
|
|
48
|
+
# max chunk size, it will still get sent out.
|
|
49
|
+
DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024
|
|
50
|
+
|
|
27
51
|
def initialize(apis, default_api, logger:)
|
|
28
52
|
@apis = apis
|
|
29
53
|
@logger = logger
|
|
@@ -36,9 +60,45 @@ module Datadog
|
|
|
36
60
|
end
|
|
37
61
|
|
|
38
62
|
def send_input(payload, tags)
|
|
39
|
-
|
|
40
|
-
parcel = EncodedParcel.new(json)
|
|
63
|
+
# Tags are the same for all chunks, serialize them one time.
|
|
41
64
|
serialized_tags = Core::TagBuilder.serialize_tags(tags)
|
|
65
|
+
|
|
66
|
+
encoder = Core::Encoding::JSONEncoder
|
|
67
|
+
encoded_snapshots = Core::Utils::Array.filter_map(payload) do |snapshot|
|
|
68
|
+
encoded = encoder.encode(snapshot)
|
|
69
|
+
if encoded.length > MAX_SERIALIZED_SNAPSHOT_SIZE
|
|
70
|
+
# Drop the snapshot.
|
|
71
|
+
# TODO report via telemetry metric?
|
|
72
|
+
logger.debug { "di: dropping too big snapshot" }
|
|
73
|
+
nil
|
|
74
|
+
else
|
|
75
|
+
encoded
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Datadog::Core::Chunker.chunk_by_size(
|
|
80
|
+
encoded_snapshots, DEFAULT_CHUNK_SIZE,
|
|
81
|
+
).each do |chunk|
|
|
82
|
+
# We drop snapshots that are too big earlier.
|
|
83
|
+
# The limit on chunked payload length here is greater
|
|
84
|
+
# than the limit on snapshot size, therefore no chunks
|
|
85
|
+
# can exceed limits here.
|
|
86
|
+
chunked_payload = encoder.join(chunk)
|
|
87
|
+
|
|
88
|
+
# We need to rescue exceptions for each chunk so that
|
|
89
|
+
# subsequent chunks are attempted to be sent.
|
|
90
|
+
begin
|
|
91
|
+
send_input_chunk(chunked_payload, serialized_tags)
|
|
92
|
+
rescue => exc
|
|
93
|
+
logger.debug { "di: failed to send snapshot chunk: #{exc.class}: #{exc} (at #{exc.backtrace.first})" }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
payload
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def send_input_chunk(chunked_payload, serialized_tags)
|
|
101
|
+
parcel = EncodedParcel.new(chunked_payload)
|
|
42
102
|
request = Request.new(parcel, serialized_tags)
|
|
43
103
|
|
|
44
104
|
response = @client.send_input_payload(request)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'ext'
|
|
4
4
|
require_relative 'noop_evaluator'
|
|
5
|
+
require_relative 'native_evaluator'
|
|
5
6
|
require_relative 'resolution_details'
|
|
6
7
|
|
|
7
8
|
module Datadog
|
|
@@ -10,7 +11,7 @@ module Datadog
|
|
|
10
11
|
class EvaluationEngine
|
|
11
12
|
ReconfigurationError = Class.new(StandardError)
|
|
12
13
|
|
|
13
|
-
ALLOWED_TYPES = %
|
|
14
|
+
ALLOWED_TYPES = %i[boolean string number float integer object].freeze
|
|
14
15
|
|
|
15
16
|
def initialize(reporter, telemetry:, logger:)
|
|
16
17
|
@reporter = reporter
|
|
@@ -20,7 +21,7 @@ module Datadog
|
|
|
20
21
|
@evaluator = NoopEvaluator.new(nil)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
def fetch_value(flag_key
|
|
24
|
+
def fetch_value(flag_key, default_value:, expected_type:, evaluation_context: nil)
|
|
24
25
|
unless ALLOWED_TYPES.include?(expected_type)
|
|
25
26
|
message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
|
|
26
27
|
return ResolutionDetails.build_error(
|
|
@@ -28,8 +29,10 @@ module Datadog
|
|
|
28
29
|
)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
context = evaluation_context&.fields
|
|
32
|
-
result = @evaluator.get_assignment(
|
|
32
|
+
context = evaluation_context&.fields.to_h
|
|
33
|
+
result = @evaluator.get_assignment(
|
|
34
|
+
flag_key, default_value: default_value, context: context, expected_type: expected_type
|
|
35
|
+
)
|
|
33
36
|
|
|
34
37
|
@reporter.report(result, flag_key: flag_key, context: evaluation_context)
|
|
35
38
|
|
|
@@ -38,19 +41,26 @@ module Datadog
|
|
|
38
41
|
@telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')
|
|
39
42
|
|
|
40
43
|
ResolutionDetails.build_error(
|
|
41
|
-
value: default_value, error_code: Ext::
|
|
44
|
+
value: default_value, error_code: Ext::GENERAL, error_message: e.message
|
|
42
45
|
)
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# NOTE: In a currect implementation configuration is expected to be a raw
|
|
49
|
+
# JSON string containing feature flags (straight from the remote config)
|
|
50
|
+
# in the format expected by `libdatadog` without any modifications
|
|
45
51
|
def reconfigure!(configuration)
|
|
46
|
-
|
|
52
|
+
if configuration.nil?
|
|
53
|
+
@logger.debug('OpenFeature: Removing configuration')
|
|
54
|
+
|
|
55
|
+
return @evaluator = NoopEvaluator.new(configuration)
|
|
56
|
+
end
|
|
47
57
|
|
|
48
|
-
@evaluator =
|
|
58
|
+
@evaluator = NativeEvaluator.new(configuration)
|
|
49
59
|
rescue => e
|
|
50
60
|
message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'
|
|
51
61
|
|
|
52
|
-
@logger.error("#{message},
|
|
53
|
-
@telemetry.report(e, description: message)
|
|
62
|
+
@logger.error("#{message}, #{e.class}: #{e.message}")
|
|
63
|
+
@telemetry.report(e, description: "#{message} (#{e.class})")
|
|
54
64
|
|
|
55
65
|
raise ReconfigurationError, e.message
|
|
56
66
|
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
|
|
@@ -11,14 +11,14 @@ module Datadog
|
|
|
11
11
|
# no-op
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def get_assignment(_flag_key, default_value
|
|
14
|
+
def get_assignment(_flag_key, default_value:, context:, expected_type:)
|
|
15
15
|
ResolutionDetails.new(
|
|
16
16
|
value: default_value,
|
|
17
17
|
log?: false,
|
|
18
18
|
error?: true,
|
|
19
19
|
error_code: Ext::PROVIDER_NOT_READY,
|
|
20
|
-
error_message: 'Waiting for
|
|
21
|
-
reason: Ext::
|
|
20
|
+
error_message: 'Waiting for flags configuration',
|
|
21
|
+
reason: Ext::ERROR
|
|
22
22
|
)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
@@ -68,27 +68,27 @@ module Datadog
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def fetch_boolean_value(flag_key:, default_value:, evaluation_context: nil)
|
|
71
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
71
|
+
evaluate(flag_key, default_value: default_value, expected_type: :boolean, evaluation_context: evaluation_context)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def fetch_string_value(flag_key:, default_value:, evaluation_context: nil)
|
|
75
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
75
|
+
evaluate(flag_key, default_value: default_value, expected_type: :string, evaluation_context: evaluation_context)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def fetch_number_value(flag_key:, default_value:, evaluation_context: nil)
|
|
79
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
79
|
+
evaluate(flag_key, default_value: default_value, expected_type: :number, evaluation_context: evaluation_context)
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def fetch_integer_value(flag_key:, default_value:, evaluation_context: nil)
|
|
83
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
83
|
+
evaluate(flag_key, default_value: default_value, expected_type: :integer, evaluation_context: evaluation_context)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def fetch_float_value(flag_key:, default_value:, evaluation_context: nil)
|
|
87
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
87
|
+
evaluate(flag_key, default_value: default_value, expected_type: :float, evaluation_context: evaluation_context)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
def fetch_object_value(flag_key:, default_value:, evaluation_context: nil)
|
|
91
|
-
evaluate(flag_key, default_value: default_value, expected_type:
|
|
91
|
+
evaluate(flag_key, default_value: default_value, expected_type: :object, evaluation_context: evaluation_context)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
private
|
|
@@ -98,7 +98,7 @@ module Datadog
|
|
|
98
98
|
return component_not_configured_default(default_value) if engine.nil?
|
|
99
99
|
|
|
100
100
|
result = engine.fetch_value(
|
|
101
|
-
flag_key
|
|
101
|
+
flag_key,
|
|
102
102
|
default_value: default_value,
|
|
103
103
|
expected_type: expected_type,
|
|
104
104
|
evaluation_context: evaluation_context
|
|
@@ -106,7 +106,7 @@ module Datadog
|
|
|
106
106
|
|
|
107
107
|
if result.error?
|
|
108
108
|
return ::OpenFeature::SDK::Provider::ResolutionDetails.new(
|
|
109
|
-
value:
|
|
109
|
+
value: default_value,
|
|
110
110
|
error_code: result.error_code,
|
|
111
111
|
error_message: result.error_message,
|
|
112
112
|
reason: result.reason
|
|
@@ -119,6 +119,13 @@ module Datadog
|
|
|
119
119
|
reason: result.reason,
|
|
120
120
|
flag_metadata: result.flag_metadata
|
|
121
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
|
+
)
|
|
122
129
|
end
|
|
123
130
|
|
|
124
131
|
def component_not_configured_default(value)
|
|
@@ -25,7 +25,7 @@ module Datadog
|
|
|
25
25
|
matcher = Core::Remote::Dispatcher::Matcher::Product.new(FFE_PRODUCTS)
|
|
26
26
|
receiver = Core::Remote::Dispatcher::Receiver.new(matcher) do |repository, changes|
|
|
27
27
|
engine = OpenFeature.engine
|
|
28
|
-
|
|
28
|
+
next unless engine
|
|
29
29
|
|
|
30
30
|
changes.each do |change|
|
|
31
31
|
content = repository[change.path]
|