Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -2876,6 +2876,25 @@ In addition, all metrics include the following tags:
| `language` | Programming language traced. (e.g. `ruby`) |
| `service` | List of services this associated with this metric. |

### Feature Flags

The OpenFeature provider supports agentless configuration delivery by default. Enabling Feature Flags alone does not contact Datadog; delivery starts when application code adopts the provider.
Comment thread
pavlokhrebto marked this conversation as resolved.

Configure Feature Flags with environment variables or inside a `Datadog.configure` block:

| Setting | Environment variable | Type | Description | Default |
|---|---|---|---|---|
| `c.feature_flags.enabled` | `DD_FEATURE_FLAGS_ENABLED` | `Boolean` | Enables or disables Feature Flags. `false` takes precedence over every configuration source. | `true` |
| `c.feature_flags.configuration_source` | `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE` | `String` | Selects `agentless`, `remote_config`, or `offline`. Values are case-insensitive and surrounding whitespace is ignored; unsupported values disable Feature Flags. | `agentless` |
| `c.feature_flags.agentless.base_url` | `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL` | `String` | Overrides the agentless endpoint. Origin URLs receive the standard configuration path; URLs with a non-root path are used verbatim. | `nil` |
| `c.feature_flags.agentless.poll_interval_seconds` | `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS` | `Integer` | Sets the agentless polling interval in seconds. Must be greater than `0`. | `30` |
| `c.feature_flags.agentless.request_timeout_seconds` | `DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS` | `Integer` | Sets the agentless request timeout in seconds, from `1` through `300`. | `5` |
| `c.feature_flags.initialization_timeout_ms` | `DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS` | `Integer` | Sets how long provider initialization waits for the first configuration, from `1` through `2147483647` milliseconds. | `30000` |

`DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED` and `c.open_feature.enabled` are deprecated. When neither stable setting is explicitly configured, the legacy value `true` selects `remote_config` and `false` disables Feature Flags.

The API key is sent only to the managed Datadog endpoint. A custom agentless URL is treated as opaque, and its query string is preserved without automatically adding `dd_env`.

### Profiling

`datadog` can produce profiles that measure method-level application resource usage within production environments. These profiles can give insight into resources spent in Ruby code outside of existing trace instrumentation.
Expand Down
6 changes: 6 additions & 0 deletions lib/datadog/core/configuration/supported_configurations.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions lib/datadog/open_feature/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ module OpenFeature
module Configuration
# A settings class for the OpenFeature component.
module Settings
DEFAULT_POLL_INTERVAL_SECONDS = 30
DEFAULT_REQUEST_TIMEOUT_SECONDS = 5
DEFAULT_INITIALIZATION_TIMEOUT_MS = 30_000

MAX_REQUEST_TIMEOUT_SECONDS = 300
MAX_INITIALIZATION_TIMEOUT_MS = 2_147_483_647

def self.configure_integer_option(option, environment_variable:, default:, setting_name:)
option.type :int
option.env environment_variable
option.env_parser do |value|
Integer(value, 10)
rescue ArgumentError
Datadog.logger.warn("#{setting_name} must be an integer; using the default")
default
end
option.default default
end

def self.extended(base)
base = base.singleton_class unless base.is_a?(Class)
add_settings!(base)
Expand All @@ -15,10 +34,19 @@ def self.add_settings!(base)
# Steep does not update `self` for this `class_eval` block.
# @type self: Datadog::Core::Configuration::Base::_DslContext
settings :open_feature do
Comment thread
TonyCTHsu marked this conversation as resolved.
# Deprecated switch retained for existing Remote Configuration adopters.
option :enabled do |o|
o.type :bool
o.env "DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED"
o.default false
o.after_set do |_, _, precedence|
next if precedence == Core::Configuration::Option::Precedence::DEFAULT

Datadog.logger.warn(
"DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED is deprecated; use " \
"DD_FEATURE_FLAGS_ENABLED and DD_FEATURE_FLAGS_CONFIGURATION_SOURCE instead"
)
end
end

# Opt-in gate for APM feature-flag span enrichment. When enabled,
Expand All @@ -42,6 +70,96 @@ def self.add_settings!(base)
o.default true
end
end

settings :feature_flags do
option :enabled do |o|
o.type :bool
o.env "DD_FEATURE_FLAGS_ENABLED"
o.default true
end

option :configuration_source do |o|
o.type :string
o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE"
o.default "agentless"
o.setter do |value|
value.to_s.strip.downcase
end
end

settings :agentless do
option :base_url do |o|
o.type :string, nilable: true
o.env "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL"
o.skip_telemetry true
o.setter do |value|
next unless value

stripped_value = value.to_s.strip
stripped_value.empty? ? nil : stripped_value
end
end

option :poll_interval_seconds do |o|
Settings.configure_integer_option(
o,
environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS",
default: Settings::DEFAULT_POLL_INTERVAL_SECONDS,
setting_name: "Feature Flags agentless poll interval",
)
o.setter do |value|
if value.is_a?(Integer) && value > 0
value
else
Datadog.logger.warn(
"Feature Flags agentless poll interval must be positive; using the default"
)
Settings::DEFAULT_POLL_INTERVAL_SECONDS
end
end
end

option :request_timeout_seconds do |o|
Settings.configure_integer_option(
o,
environment_variable: "DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS",
default: Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS,
setting_name: "Feature Flags agentless request timeout",
)
o.setter do |value|
if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_REQUEST_TIMEOUT_SECONDS
value
else
Datadog.logger.warn(
"Feature Flags agentless request timeout must be within (0, " \
"#{Settings::MAX_REQUEST_TIMEOUT_SECONDS}]; using the default"
)
Settings::DEFAULT_REQUEST_TIMEOUT_SECONDS
end
end
end
end

option :initialization_timeout_ms do |o|
Comment thread
TonyCTHsu marked this conversation as resolved.
Settings.configure_integer_option(
o,
environment_variable: "DD_EXPERIMENTAL_FLAGGING_PROVIDER_INITIALIZATION_TIMEOUT_MS",
default: Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS,
setting_name: "Feature Flags provider initialization timeout",
)
o.setter do |value|
if value.is_a?(Integer) && value > 0 && value <= Settings::MAX_INITIALIZATION_TIMEOUT_MS
value
else
Datadog.logger.warn(
"Feature Flags provider initialization timeout must be within (0, " \
"#{Settings::MAX_INITIALIZATION_TIMEOUT_MS}]; using the default"
)
Settings::DEFAULT_INITIALIZATION_TIMEOUT_MS
end
end
end
end
end
end
end
Expand Down
90 changes: 90 additions & 0 deletions lib/datadog/open_feature/configuration/agentless_endpoint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require "uri"

module Datadog
module OpenFeature
module Configuration
# Validated endpoint for agentless Feature Flags configuration delivery.
class AgentlessEndpoint
DEFAULT_SITE = "datadoghq.com"
AGENTLESS_SUBDOMAIN = "ufc-server.ff-cdn"
CONFIGURATION_PATH = "/api/v2/feature-flagging/config/rules-based/server"
INVALID_SITE_CHARACTERS = /[\s\/?#@:]/
INTERNAL_WHITESPACE = /\s/

class << self
def build(site:, environment:, base_url: nil, logger: Datadog.logger)
if base_url
build_custom(base_url, logger)
else
build_managed(site, environment, logger)
end
end

private

def build_custom(base_url, logger)
unless base_url.ascii_only?
logger.warn("Feature Flags agentless base URL is invalid; agentless delivery is disabled")
return
end

if base_url.match?(INTERNAL_WHITESPACE)
logger.warn("Feature Flags agentless base URL contains whitespace; agentless delivery is disabled")
return
end

uri = URI.parse(base_url)
unless uri.is_a?(URI::HTTP) && uri.absolute? && uri.host
logger.warn("Feature Flags agentless base URL must be an absolute HTTP or HTTPS URL; agentless delivery is disabled")
return
end

uri.path = CONFIGURATION_PATH if uri.path.to_s.empty? || uri.path == "/"
new(uri, managed: false)
rescue URI::InvalidURIError
logger.warn("Feature Flags agentless base URL is invalid; agentless delivery is disabled")
nil
end

def build_managed(site, environment, logger)
string_site = site.to_s
unless string_site.ascii_only?
logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
return
end

normalized_site = string_site.strip.downcase
normalized_site = DEFAULT_SITE if normalized_site.empty?
if normalized_site.match?(INVALID_SITE_CHARACTERS)
logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
return
end

uri = URI::HTTPS.build(
host: "#{AGENTLESS_SUBDOMAIN}.#{normalized_site}",
path: CONFIGURATION_PATH,
query: environment.nil? ? nil : URI.encode_www_form(dd_env: environment),
)
new(uri, managed: true)
rescue URI::InvalidComponentError
logger.warn("Feature Flags site is invalid; agentless delivery is disabled")
nil
end
end

attr_reader :uri

def initialize(uri, managed:)
@uri = uri
@managed = managed
end

def managed?
@managed
end
end
end
end
end
43 changes: 43 additions & 0 deletions lib/datadog/open_feature/configuration/source.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require_relative "source/resolution"

module Datadog
module OpenFeature
module Configuration
module Source
AGENTLESS = "agentless"
REMOTE_CONFIG = "remote_config"
OFFLINE = "offline"

class << self
def resolve(settings, logger: Datadog.logger)
Comment thread
pavlokhrebto marked this conversation as resolved.
feature_flags = settings.feature_flags
source = resolve_source(feature_flags, legacy: settings.open_feature, logger: logger)
enabled = feature_flags.enabled && source != OFFLINE

Resolution.new(source, enabled: enabled)
end

private

def resolve_source(settings, legacy:, logger:)
configured_source = settings.configuration_source
if !settings.using_default?(:configuration_source) && !configured_source.empty?
return configured_source if configured_source == AGENTLESS ||
configured_source == REMOTE_CONFIG ||
configured_source == OFFLINE

logger.warn("Unsupported Feature Flags configuration source; Feature Flags are disabled")
OFFLINE
elsif !legacy.using_default?(:enabled)
legacy.enabled ? REMOTE_CONFIG : OFFLINE
else
AGENTLESS
end
end
end
end
end
end
end
22 changes: 22 additions & 0 deletions lib/datadog/open_feature/configuration/source/resolution.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Configuration
module Source
class Resolution
attr_reader :source

def initialize(source, enabled:)
@source = source
@enabled = enabled
end

def enabled?
@enabled
end
end
end
end
end
end
Loading
Loading