-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Core configuration add env var and casting functionality #2970
Changes from all commits
602f7f1
aaddc4c
8bcd916
61d37b7
750c721
cd33917
81c2b96
3aa907f
0fae4d7
00e1bc4
d8d6755
d09e2fd
15246fc
765a287
d6ab986
6cacb85
345454a
87d739d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,10 +33,9 @@ def self.add_settings!(base) | |
base.class_eval do | ||
settings :appsec do | ||
option :enabled do |o| | ||
o.default { env_to_bool('DD_APPSEC_ENABLED', DEFAULT_APPSEC_ENABLED) } | ||
o.setter do |v| | ||
v ? true : false | ||
end | ||
o.type :bool | ||
o.env 'DD_APPSEC_ENABLED' | ||
o.default DEFAULT_APPSEC_ENABLED | ||
end | ||
|
||
define_method(:instrument) do |integration_name| | ||
|
@@ -53,73 +52,71 @@ def self.add_settings!(base) | |
end | ||
|
||
option :ruleset do |o| | ||
o.default { ENV.fetch('DD_APPSEC_RULES', DEFAULT_APPSEC_RULESET) } | ||
o.env 'DD_APPSEC_RULES' | ||
o.default DEFAULT_APPSEC_RULESET | ||
end | ||
|
||
option :ip_denylist do |o| | ||
o.default { [] } | ||
o.type :array | ||
o.default [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a difference between block and value here:
This led me to wonder if we should This then led me to wonder if we should in fact (transparently) WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In (I had wondered the same thing you did and ended up spotting it) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to wrap it on describe '#writer_options' do
subject(:writer_options) { settings.tracing.test_mode.writer_options }
it { is_expected.to eq({}) }
context 'when modified' do
it 'does not modify the default by reference' do
settings.tracing.test_mode.writer_options[:foo] = :bar
expect(settings.tracing.test_mode.writer_options).to_not be_empty
expect(settings.tracing.test_mode.options[:writer_options].default_value).to be_empty
end
end
end The options were like this before our changes: option :writer_options do |o|
o.default { {} }
end This is how they are after our changes: option :writer_options do |o|
o.type :hash
o.default({})
end So I believe this was an oversight on my part in which using a default value as a block also meant that calling @lloeki suggested that we should be freezing the configuration value it gets set, so any modification to the configuration value should have to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think freezing instead of dup'ing seems reasonable as well, although I wonder if somewhere we may be doing something weird that doesn't like it (but perhaps in that situation it'd be worth fixing that code instead of not freezing) |
||
end | ||
|
||
option :user_id_denylist do |o| | ||
o.default { [] } | ||
o.type :array | ||
o.default [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm wondering if we should have logic that would have Then, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a great suggestion and will certainly reduce the amount of boilerplate. I would leave that exercise for a subsequent PR |
||
end | ||
|
||
option :waf_timeout do |o| | ||
o.default { ENV.fetch('DD_APPSEC_WAF_TIMEOUT', DEFAULT_APPSEC_WAF_TIMEOUT) } # us | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
o.env 'DD_APPSEC_WAF_TIMEOUT' # us | ||
o.default DEFAULT_APPSEC_WAF_TIMEOUT | ||
o.setter do |v| | ||
Datadog::Core::Utils::Duration.call(v.to_s, base: :us) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was already like this (so I'd say this comment is not for this PR), but wow damn this helper is extremely complex for something that is only parsed as integer-microseconds in our other libraries (and this helper is only used once!). Consider maybe going back to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Except in Go, coz it's parsed as a But they fixed since by appending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm wondering if an alternative would be to have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the extra context! On the topic of |
||
end | ||
end | ||
|
||
option :waf_debug do |o| | ||
o.default { env_to_bool('DD_APPSEC_WAF_DEBUG', DEFAULT_APPSEC_WAF_DEBUG) } | ||
o.setter do |v| | ||
v ? true : false | ||
end | ||
o.env 'DD_APPSEC_WAF_DEBUG' | ||
o.default DEFAULT_APPSEC_WAF_DEBUG | ||
o.type :bool | ||
end | ||
|
||
option :trace_rate_limit do |o| | ||
o.default { env_to_int('DD_APPSEC_TRACE_RATE_LIMIT', DEFAULT_APPSEC_TRACE_RATE_LIMIT) } # trace/s | ||
o.type :int | ||
o.env 'DD_APPSEC_TRACE_RATE_LIMIT' # trace/s | ||
o.default DEFAULT_APPSEC_TRACE_RATE_LIMIT | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was already like this, so no need to change in this PR, but is it just me that finds weird this structure of splitting the definition of the defaults at the top of the file with the definition of the setting: DEFAULT_APPSEC_WAF_DEBUG = false
# ...
option :trace_rate_limit do |o|
o.type :int
o.env_var 'DD_APPSEC_TRACE_RATE_LIMIT' # trace/s
o.default DEFAULT_APPSEC_TRACE_RATE_LIMIT
end I kinda prefer how we do it in other places: option :force_enable_gc_profiling do |o|
o.env_var 'DD_PROFILING_FORCE_ENABLE_GC'
o.type :bool
o.default false
end ...as it avoids the redundancy + avoids any other parts of the code accessing the default constants where they should always go through the settings object. I claim the settings should be the source of truth for the defaults. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will cleanup in a separate PR |
||
end | ||
|
||
option :obfuscator_key_regex do |o| | ||
o.default { ENV.fetch('DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP', DEFAULT_OBFUSCATOR_KEY_REGEX) } | ||
o.type :string | ||
o.env 'DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP' | ||
o.default DEFAULT_OBFUSCATOR_KEY_REGEX | ||
end | ||
|
||
option :obfuscator_value_regex do |o| | ||
o.default do | ||
ENV.fetch( | ||
'DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP', | ||
DEFAULT_OBFUSCATOR_VALUE_REGEX | ||
) | ||
end | ||
o.type :string | ||
o.env 'DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP' | ||
o.default DEFAULT_OBFUSCATOR_VALUE_REGEX | ||
end | ||
|
||
settings :track_user_events do | ||
option :enabled do |o| | ||
o.default do | ||
ENV.fetch( | ||
'DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING', | ||
DEFAULT_APPSEC_AUTOMATED_TRACK_USER_EVENTS_ENABLED | ||
) | ||
end | ||
o.setter do |v| | ||
if v | ||
v.to_s != 'disabled' | ||
o.env 'DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING' | ||
o.default DEFAULT_APPSEC_AUTOMATED_TRACK_USER_EVENTS_ENABLED | ||
o.setter do |value| | ||
if value.is_a?(String) | ||
value != 'disabled' | ||
else | ||
false | ||
!!value # rubocop:disable Style/DoubleNegation | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
end | ||
|
||
option :mode do |o| | ||
o.default do | ||
ENV.fetch('DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING', DEFAULT_APPSEC_AUTOMATED_TRACK_USER_EVENTS_MODE) | ||
end | ||
o.env 'DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING' | ||
o.default DEFAULT_APPSEC_AUTOMATED_TRACK_USER_EVENTS_MODE | ||
o.setter do |v| | ||
string_value = v.to_s | ||
if APPSEC_VALID_TRACK_USER_EVENTS_MODE.include?(string_value) | ||
string_value | ||
if APPSEC_VALID_TRACK_USER_EVENTS_MODE.include?(v.to_s) | ||
v.to_s | ||
else | ||
Datadog.logger.warn( | ||
'The appsec.track_user_events.mode value provided is not supported.' \ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
# frozen_string_literal: true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Meta note on this PR: This is the kind of PR that has the potential to have undetected conflicts with other ongoing PRs -- e.g. any PRs that touch settings that branched out before this PR have the potential to cause issues. Here's my suggestion -- before merging this PR to master:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Meta-question: this has nothing to do with the Meta-suggestion: maybe it should go as a review comment in "Finish your review"? but then it's not a "threaded" discussion I guess (subsequent discussion would happen in the global PR discussion tab). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, it's for the whole review, and I only picked elsewhere because I find people sometimes miss the comment posted with the review itself, and I thought this comment was important enough not to be missed. Plus, as you pointed out, it's really annoying to not have threading for those comments. So I'm just working around GitHub UX limitations >_> |
||
|
||
require_relative '../utils/safe_dup' | ||
|
||
module Datadog | ||
module Core | ||
module Configuration | ||
|
@@ -49,7 +51,7 @@ def set(value, precedence: Precedence::PROGRAMMATIC) | |
end | ||
|
||
old_value = @value | ||
(@value = context_exec(value, old_value, &definition.setter)).tap do |v| | ||
(@value = context_exec(validate_type(value), old_value, &definition.setter)).tap do |v| | ||
@is_set = true | ||
@precedence_set = precedence | ||
context_exec(v, old_value, &definition.on_set) if definition.on_set | ||
|
@@ -62,7 +64,7 @@ def get | |
elsif definition.delegate_to | ||
context_eval(&definition.delegate_to) | ||
else | ||
set(default_value, precedence: Precedence::DEFAULT) | ||
set_value_from_env_or_default | ||
end | ||
end | ||
|
||
|
@@ -84,7 +86,7 @@ def default_value | |
if definition.default.instance_of?(Proc) | ||
context_eval(&definition.default) | ||
else | ||
definition.experimental_default_proc || definition.default | ||
definition.experimental_default_proc || Core::Utils::SafeDup.frozen_or_dup(definition.default) | ||
end | ||
end | ||
|
||
|
@@ -94,6 +96,93 @@ def default_precedence? | |
|
||
private | ||
|
||
def coerce_env_variable(value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the intention here is to replace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with that. There is still one setting that still uses settings :client_ip do
# Whether client IP collection is enabled. When enabled client IPs from HTTP requests will
# be reported in traces.
#
# Usage of the DD_TRACE_CLIENT_IP_HEADER_DISABLED environment variable is deprecated.
#
# @see https://docs.datadoghq.com/tracing/configure_data_security#configuring-a-client-ip-header
#
# @default `DD_TRACE_CLIENT_IP_ENABLED` environment variable, otherwise `false`.
# @return [Boolean]
option :enabled do |o|
o.type :bool
o.default do
disabled = env_to_bool(Tracing::Configuration::Ext::ClientIp::ENV_DISABLED)
enabled = if disabled.nil?
false
else
Datadog::Core.log_deprecation do
"#{Tracing::Configuration::Ext::ClientIp::ENV_DISABLED} environment variable is deprecated, use #{Tracing::Configuration::Ext::ClientIp::ENV_ENABLED} instead."
end
!disabled
end
# ENABLED env var takes precedence over deprecated DISABLED
env_to_bool(Tracing::Configuration::Ext::ClientIp::ENV_ENABLED, enabled)
end
end
end I guess it is ok to mark the methods from I would like to that on separate PR to avoid touching more parts of the codebase on this single PR |
||
return context_exec(value, &@definition.env_parser) if @definition.env_parser | ||
|
||
case @definition.type | ||
when :int | ||
# DEV-2.0: Change to a more strict coercion method. Integer(value). | ||
value.to_i | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
when :float | ||
# DEV-2.0: Change to a more strict coercion method. Float(value). | ||
value.to_f | ||
when :array | ||
values = if value.include?(',') | ||
value.split(',') | ||
else | ||
value.split(' ') # rubocop:disable Style/RedundantArgument | ||
end | ||
Comment on lines
+110
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this become There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Where is that defined? The code for Doing a search on I'm happy to remove the if condition, I just to make sure to understand if we have to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean the default for the DSL should be to not support it, because in my view the "spaces separator" is the weird use-case, so it should require extra work on the part of the settings that want it. (As I mention below, accepting spaces on top of commas has the potential of creating more inconsistency across datadog libraries) |
||
|
||
values.map! do |v| | ||
v.gsub!(/\A[\s,]*|[\s,]*\Z/, '') | ||
|
||
v.empty? ? nil : v | ||
end | ||
|
||
values.compact! | ||
values | ||
Comment on lines
+109
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I recognize this slice as an inlined version of the This means that in some settings, we previously only supported "foo,bar,baz" but now we also support "foo bar baz". For legacy stuff, there's not a lot we can do, but it's kinda weird to allow the bizarre alternative format for all array-like options. So I think we should somehow restore the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very good point @ivoanjo Will see what I can do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This use cases where tackled on d6ab986 Thanks to the use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should make the separator more configurable -- this way we push everyone to use commas, and they need to go out of their way if they really need to support others (but can, if needed). Thus, the complexity is moved from the core (supporting arbitrary separators) to the setting (if you wanna do weird stuff, you pay the cost of doing weird stuff, locally). |
||
when :bool | ||
string_value = value | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
string_value = string_value.downcase | ||
string_value == 'true' || string_value == '1' # rubocop:disable Style/MultipleComparison | ||
when :string, NilClass | ||
value | ||
else | ||
raise ArgumentError, "The option #{@definition.name} is using an unknown type option `#{@definition.type}`" | ||
end | ||
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: I wonder if this should be omitted, since |
||
end | ||
|
||
def validate_type(value) | ||
raise_error = false | ||
|
||
valid_type = validate(@definition.type, value) | ||
|
||
unless valid_type | ||
raise_error = if @definition.type_options[:nilable] | ||
!value.is_a?(NilClass) | ||
else | ||
true | ||
end | ||
end | ||
|
||
if raise_error | ||
error_msg = if @definition.type_options[:nilable] | ||
"The option #{@definition.name} support this type `#{@definition.type}` "\ | ||
"and `nil` but the value provided is #{value.class}" | ||
else | ||
"The option #{@definition.name} support this type `#{@definition.type}` "\ | ||
"but the value provided is #{value.class}" | ||
end | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
raise ArgumentError, error_msg | ||
end | ||
|
||
value | ||
end | ||
|
||
def validate(type, value) | ||
case type | ||
when :string | ||
value.is_a?(String) | ||
when :int, :float | ||
value.is_a?(Numeric) | ||
when :array | ||
value.is_a?(Array) | ||
when :hash | ||
value.is_a?(Hash) | ||
when :bool | ||
value.is_a?(TrueClass) || value.is_a?(FalseClass) | ||
when :proc | ||
value.is_a?(Proc) | ||
when :symbol | ||
value.is_a?(Symbol) | ||
when NilClass | ||
true | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+179
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Maybe worth adding a comment here stating that this is a fallback for settings that don't define a type. |
||
else | ||
raise ArgumentError, "The option #{@definition.name} is using an unknown type option `#{@definition.type}`" | ||
GustavoCaso marked this conversation as resolved.
Show resolved
Hide resolved
|
||
end | ||
end | ||
|
||
def context_exec(*args, &block) | ||
@context.instance_exec(*args, &block) | ||
end | ||
|
@@ -102,6 +191,29 @@ def context_eval(&block) | |
@context.instance_eval(&block) | ||
end | ||
|
||
def set_value_from_env_or_default | ||
value = nil | ||
precedence = nil | ||
|
||
if definition.env && ENV[definition.env] | ||
value = coerce_env_variable(ENV[definition.env]) | ||
precedence = Precedence::PROGRAMMATIC | ||
end | ||
|
||
if value.nil? && definition.deprecated_env && ENV[definition.deprecated_env] | ||
value = coerce_env_variable(ENV[definition.deprecated_env]) | ||
precedence = Precedence::PROGRAMMATIC | ||
Comment on lines
+200
to
+205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That comment should be changed. If a customer has modified the configuration value via ENV variable, it has been configured programmatically since the customer is not using the default value. For example: option :enabled do |o|
o.type :bool
o.env 'DD_APPSEC_ENABLED'
o.default false
end If the customer changes the value using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Seems reasonable -- either updating the comment or perhaps introducing a new environment variable level seems OK. |
||
|
||
Datadog::Core.log_deprecation do | ||
"#{definition.deprecated_env} environment variable is deprecated, use #{definition.env} instead." | ||
end | ||
end | ||
|
||
option_value = value.nil? ? default_value : value | ||
|
||
set(option_value, precedence: precedence || Precedence::DEFAULT) | ||
end | ||
|
||
# Used for testing | ||
attr_reader :precedence_set | ||
private :precedence_set | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels weird, seems like it should read
o.default true
.Indeed whatever else depends on
DEFAULT_APPSEC_ENABLED
should instead read the configuration setting.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is legacy from when the appsec setting was not using the core configuration code.
We can remove all those constants on a separate PR 😄