Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
b567235
Add OpenFeature component
Strech Oct 22, 2025
da49ec8
Fir require of the OpenFeature component
Strech Oct 22, 2025
9bad9dd
Add skeleton for evaluation API
Strech Oct 23, 2025
105c754
Add guard-clause to the Provider and Evaluator
Strech Oct 24, 2025
9617a35
Add comment regarding GC
Strech Oct 24, 2025
a381534
Update settings and component interface impl.
Strech Oct 24, 2025
6c761bd
Add RBS files for OpenFeature component
Strech Oct 24, 2025
4ed3540
Fix typing for Remote module
Strech Oct 24, 2025
98f116c
Add openfeature-sdk stub and fix component typing
Strech Oct 24, 2025
da72ee2
Fix settings RBS
Strech Oct 24, 2025
f543d64
Wrap FFI code into a separate module
Strech Oct 27, 2025
d978e50
Add tests for remote config
Strech Oct 27, 2025
46396ec
Add component tests
Strech Oct 27, 2025
4e24ca8
Add new environment configuration definition
Strech Oct 27, 2025
11209aa
Add component specs
Strech Oct 27, 2025
e3333a5
Add testing matrix and new group
Strech Oct 28, 2025
f29acf7
Adjust evaluator specs
Strech Oct 29, 2025
ec27207
Add some basic tests for Provider
Strech Oct 29, 2025
c327543
Extract Binding module
Strech Nov 4, 2025
9c0a0a8
Rename Evaluator to be EvaluationEngine
Strech Nov 4, 2025
894248d
Update EvaluationEngine initialization interface
Strech Nov 4, 2025
d8f32fa
Rework evaluation engine name in the component
Strech Nov 4, 2025
73c6d55
Rework evaluation engine name in the component
Strech Nov 4, 2025
1257f95
Fix naming in provider
Strech Nov 4, 2025
ec25611
Add extention module with constants
Strech Nov 4, 2025
99966c5
Update component initialization
Strech Nov 4, 2025
0a4a399
Add reconfiguration mutex to the evaluation engine
Strech Nov 5, 2025
1fbc831
Make engine dynamic inside provider
Strech Nov 7, 2025
4060236
Add no-op evaluator to the engine
Strech Nov 7, 2025
d481342
Update error and success results structure
Strech Nov 7, 2025
04cf79f
Introduce Binding::ResolutionDetails object
Strech Nov 7, 2025
c579c33
Add transport and exposures data model
Strech Oct 31, 2025
95a6041
Clean exposure events transport behavior
Strech Oct 31, 2025
b2a5cbe
Clean http client
Strech Oct 31, 2025
f9a87a7
Clean transport HTTP API
Strech Oct 31, 2025
832eb5e
Clean up entire transport folder
Strech Oct 31, 2025
c0699c5
AI: Worker and Buffer initial implementation
Strech Nov 3, 2025
c9e02bb
AI: Adjustments to the worker and buffer interface
Strech Nov 3, 2025
3a7c475
AI: Add Core::Queue instead of manual operations
Strech Nov 3, 2025
5ceb571
Adjust buffer implementation and correct specs
Strech Nov 3, 2025
5771e4c
AI: Add tests for worker class
Strech Nov 3, 2025
8710948
Refactor worker and cover behavior with tests
Strech Nov 4, 2025
4383006
Add RBS files for transport and worker
Strech Nov 4, 2025
aa7498e
Fix worker perform interface
Strech Nov 4, 2025
1d31752
Create Reporter for exposures reporting
Strech Nov 5, 2025
659d81c
Introduce deduplicator and reporter
Strech Nov 5, 2025
42e6f5d
Rework Reporter and the data model
Strech Nov 5, 2025
c73be52
Refactor batching of exposure events
Strech Nov 6, 2025
971c806
Fix tests after rebase
Strech Nov 7, 2025
4872fca
Use Binding::ResolutionDetails everywhere
Strech Nov 7, 2025
330f0d0
Add reporting to the evaluation
Strech Nov 10, 2025
3e98e02
Fix tests after rebase
Strech Nov 10, 2025
73f8601
Adjust code to be thread-safe
Strech Nov 10, 2025
f8af1a2
Update RBS definitions
Strech Nov 10, 2025
b4fa4b6
Address PR feedback
Strech Nov 10, 2025
27a9ace
Change RC and reconfiguration flow
Strech Nov 10, 2025
f39e6aa
Fix standardrb complaints
Strech Nov 10, 2025
ea0776a
Fix duplication issue in components
Strech Nov 11, 2025
62e6a0e
Fix loading specs
Strech Nov 11, 2025
e814c6a
Remove stale types
Strech Nov 11, 2025
7b3d536
Add additional test case for RC being disabled
Strech Nov 11, 2025
1ba10f1
Add missing RBS definitions
Strech Nov 11, 2025
a4a3ffb
Apply suggested changes from the review
Strech Nov 11, 2025
e2a6bd6
Update lib/datadog/open_feature/component.rb
Strech Nov 11, 2025
fdc8975
Reduce boilerplate and unnecessary files
Strech Nov 11, 2025
e2ea13a
Fix worker flushing on demand and shutdown
Strech Nov 11, 2025
bf101c6
Remove unnecessary methods from batch builder
Strech Nov 11, 2025
057168c
Add helper methods to the resolution details
Strech Nov 12, 2025
ed55b41
Remove unused evaluation engine exposed methods
Strech Nov 12, 2025
ca515b4
Fix issues raised by standardrb
Strech Nov 12, 2025
d0212d8
Update provider documentation
Strech Nov 12, 2025
0fcbd14
Update spec/datadog/open_feature/remote_spec.rb
Strech Nov 12, 2025
cf7425e
Update lib/datadog/open_feature/component.rb
Strech Nov 12, 2025
8420179
Update lib/datadog/open_feature/exposures/worker.rb
Strech Nov 12, 2025
0ab7091
Update lib/datadog/open_feature/evaluation_engine.rb
Strech Nov 12, 2025
fe83a47
Update lib/datadog/open_feature/exposures/reporter.rb
Strech Nov 12, 2025
b375017
Update lib/datadog/open_feature/exposures/event.rb
Strech Nov 12, 2025
6025613
Update lib/datadog/open_feature/exposures/worker.rb
Strech Nov 12, 2025
6609310
Update lib/datadog/open_feature/transport/exposures.rb
Strech Nov 12, 2025
db96bdf
Update lib/datadog/open_feature/transport/http/exposures.rb
Strech Nov 12, 2025
e572afc
Fix edge-cases mentioned in review
Strech Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Style:
- "spec/datadog/kit/**/**"
- "spec/datadog/profiling*"
- "spec/datadog/profiling/**/*"
- "spec/datadog/open_feature*"
- "spec/datadog/open_feature/**/*"
- "yard/**/*.rb"

Layout:
Expand Down
2 changes: 1 addition & 1 deletion appraisal/ruby-3.4.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@
gem 'resque'
gem 'roda', '>= 2.0.0'
gem 'semantic_logger', '~> 4.0'
# Note: Sidekiq 8 uses different timestamp formatting compared to prior versions. As long as
# NOTE: Sidekiq 8 uses different timestamp formatting compared to prior versions. As long as
# versions <8 are supported, make sure there's some CI running both older and newer versions.
gem 'sidekiq', '~> 8'
gem 'sneakers', '>= 2.12.0'
Expand Down
1 change: 1 addition & 0 deletions lib/datadog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require_relative 'datadog/appsec'
require_relative 'datadog/di'
require_relative 'datadog/data_streams'
require_relative 'datadog/open_feature'

# Line probes will not work on Ruby < 2.6 because of lack of :script_compiled
# trace point. Activate DI automatically on supported Ruby versions but
Expand Down
8 changes: 7 additions & 1 deletion lib/datadog/core/configuration/components.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require_relative '../../profiling/component'
require_relative '../../appsec/component'
require_relative '../../di/component'
require_relative '../../open_feature/component'
require_relative '../../error_tracking/component'
require_relative '../crashtracking/component'
require_relative '../environment/agent_info'
Expand Down Expand Up @@ -106,7 +107,8 @@ def build_data_streams(settings, agent_settings, logger)
:dynamic_instrumentation,
:appsec,
:agent_info,
:data_streams
:data_streams,
:open_feature

def initialize(settings)
@settings = settings
Expand Down Expand Up @@ -140,6 +142,7 @@ def initialize(settings)
@runtime_metrics = self.class.build_runtime_metrics_worker(settings, @logger, telemetry)
@health_metrics = self.class.build_health_metrics(settings, @logger, telemetry)
@appsec = Datadog::AppSec::Component.build_appsec_component(settings, telemetry: telemetry)
@open_feature = OpenFeature::Component.build(settings, agent_settings, logger: @logger, telemetry: telemetry)
@dynamic_instrumentation = Datadog::DI::Component.build(settings, agent_settings, @logger, telemetry: telemetry)
@error_tracking = Datadog::ErrorTracking::Component.build(settings, @tracer, @logger)
@data_streams = self.class.build_data_streams(settings, agent_settings, @logger)
Expand Down Expand Up @@ -199,6 +202,9 @@ def shutdown!(replacement = nil)
# Shutdown DI after remote, since remote config triggers DI operations.
dynamic_instrumentation&.shutdown!

# Shutdown OpenFeature component
open_feature&.shutdown!

# Decommission AppSec
appsec&.shutdown!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module Configuration
"DD_ENV" => {version: ["A"]},
"DD_ERROR_TRACKING_HANDLED_ERRORS" => {version: ["A"]},
"DD_ERROR_TRACKING_HANDLED_ERRORS_INCLUDE" => {version: ["A"]},
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED" => {version: ["A"]},
"DD_GIT_COMMIT_SHA" => {version: ["A"]},
"DD_GIT_REPOSITORY_URL" => {version: ["A"]},
"DD_HEALTH_METRICS_ENABLED" => {version: ["A"]},
Expand Down
7 changes: 7 additions & 0 deletions lib/datadog/core/remote/client/capabilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative '../../utils/base64'
require_relative '../../../appsec/remote'
require_relative '../../../tracing/remote'
require_relative '../../../open_feature/remote'

module Datadog
module Core
Expand Down Expand Up @@ -38,6 +39,12 @@ def register(settings)
register_receivers(Datadog::DI::Remote.receivers(@telemetry))
end

if settings.respond_to?(:open_feature) && settings.open_feature.enabled
register_capabilities(Datadog::OpenFeature::Remote.capabilities)
register_products(Datadog::OpenFeature::Remote.products)
register_receivers(Datadog::OpenFeature::Remote.receivers(@telemetry))
end

register_capabilities(Datadog::Tracing::Remote.capabilities)
register_products(Datadog::Tracing::Remote.products)
register_receivers(Datadog::Tracing::Remote.receivers(@telemetry))
Expand Down
19 changes: 19 additions & 0 deletions lib/datadog/open_feature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require_relative 'core/configuration'
require_relative 'open_feature/configuration'

module Datadog
# A namespace for the OpenFeature component.
module OpenFeature
Core::Configuration::Settings.extend(Configuration::Settings)

def self.enabled?
Datadog.configuration.open_feature.enabled
end

def self.engine
Datadog.send(:components).open_feature&.engine
end
end
end
43 changes: 43 additions & 0 deletions lib/datadog/open_feature/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative 'evaluation_engine'
require_relative 'exposures/buffer'
require_relative 'exposures/worker'
require_relative 'exposures/deduplicator'
require_relative 'exposures/reporter'
require_relative 'transport/http'

module Datadog
module OpenFeature
# This class is the entry point for the OpenFeature component
class Component
attr_reader :engine

def self.build(settings, agent_settings, logger:, telemetry:)
return unless settings.respond_to?(:open_feature) && settings.open_feature.enabled

unless settings.respond_to?(:remote) && settings.remote.enabled
message = 'OpenFeature could not be enabled as Remote Configuration is currently disabled. ' \
'To enable Remote Configuration, see https://docs.datadoghq.com/agent/remote_config'
logger.warn(message)

return
end

new(settings, agent_settings, logger: logger, telemetry: telemetry)
end

def initialize(settings, agent_settings, logger:, telemetry:)
transport = Transport::HTTP.build(agent_settings: agent_settings, logger: logger)
@worker = Exposures::Worker.new(settings: settings, transport: transport, telemetry: telemetry, logger: logger)

reporter = Exposures::Reporter.new(@worker, telemetry: telemetry, logger: logger)
@engine = EvaluationEngine.new(reporter, telemetry: telemetry, logger: logger)
end

def shutdown!
@worker.graceful_shutdown
end
end
end
end
27 changes: 27 additions & 0 deletions lib/datadog/open_feature/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Configuration
# A settings class for the OpenFeature component.
module Settings
def self.extended(base)
base = base.singleton_class unless base.is_a?(Class)
add_settings!(base)
end

def self.add_settings!(base)
base.class_eval do
settings :open_feature do
option :enabled do |o|
o.type :bool
o.env 'DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED'
o.default false
end
end
end
end
end
end
end
end
59 changes: 59 additions & 0 deletions lib/datadog/open_feature/evaluation_engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# frozen_string_literal: true

require_relative 'ext'
require_relative 'noop_evaluator'
require_relative 'resolution_details'

module Datadog
module OpenFeature
# This class performs the evaluation of the feature flag
class EvaluationEngine
ReconfigurationError = Class.new(StandardError)

ALLOWED_TYPES = %w[boolean string number float integer object].freeze

def initialize(reporter, telemetry:, logger:)
@reporter = reporter
@telemetry = telemetry
@logger = logger

@evaluator = NoopEvaluator.new(nil)
end

def fetch_value(flag_key:, default_value:, expected_type:, evaluation_context: nil)
unless ALLOWED_TYPES.include?(expected_type)
message = "unknown type #{expected_type.inspect}, allowed types #{ALLOWED_TYPES.join(", ")}"
return ResolutionDetails.build_error(
value: default_value, error_code: Ext::UNKNOWN_TYPE, error_message: message
)
end

context = evaluation_context&.fields || {}
result = @evaluator.get_assignment(flag_key, default_value, context, expected_type)

@reporter.report(result, flag_key: flag_key, context: evaluation_context)

result
rescue => e
@telemetry.report(e, description: 'OpenFeature: Failed to fetch flag value')

ResolutionDetails.build_error(
value: default_value, error_code: Ext::PROVIDER_FATAL, error_message: e.message
)
end

def reconfigure!(configuration)
@logger.debug('OpenFeature: Removing configuration') if configuration.nil?

@evaluator = NoopEvaluator.new(configuration)
rescue => e
message = 'OpenFeature: Failed to reconfigure, reverting to the previous configuration'

@logger.error("#{message}, error #{e.inspect}")
@telemetry.report(e, description: message)

raise ReconfigurationError, e.message
end
end
end
end
32 changes: 32 additions & 0 deletions lib/datadog/open_feature/exposures/batch_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Exposures
# This class builds a batch of exposures and context to be sent to the Agent
class BatchBuilder
def initialize(settings)
@context = build_context(settings)
end

def payload_for(events)
{
context: @context,
exposures: events
}
end

private

def build_context(settings)
context = {}
context[:env] = settings.env if settings.env
context[:service] = settings.service if settings.service
context[:version] = settings.version if settings.version

context
end
end
end
end
end
43 changes: 43 additions & 0 deletions lib/datadog/open_feature/exposures/buffer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative '../../core/buffer/cruby'

module Datadog
module OpenFeature
module Exposures
# This class is a buffer for exposure events that evicts at random and
# keeps track of the number of dropped events
#
# WARNING: This class does not work as intended on JRuby
class Buffer < Core::Buffer::CRuby
DEFAULT_LIMIT = 1_000

attr_reader :dropped_count

def initialize(limit = DEFAULT_LIMIT)
@dropped = 0
@dropped_count = 0

super
end

protected

def drain!
drained = super

@dropped_count = @dropped
@dropped = 0

drained
end

def replace!(item)
@dropped += 1

super
end
end
end
end
end
30 changes: 30 additions & 0 deletions lib/datadog/open_feature/exposures/deduplicator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

require_relative '../../core/utils/lru_cache'

module Datadog
module OpenFeature
module Exposures
# This class is a deduplication buffer based on LRU cache for exposure events
class Deduplicator
DEFAULT_CACHE_LIMIT = 1_000

def initialize(limit: DEFAULT_CACHE_LIMIT)
@cache = Datadog::Core::Utils::LRUCache.new(limit)
@mutex = Mutex.new
end

def duplicate?(key, value)
@mutex.synchronize do
stored = @cache[key]
return true if stored == value

@cache[key] = value
end

false
end
end
end
end
end
Loading