-
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
Add support for Option precedence #2915
Conversation
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.
Looks reasonable! I left a bunch of suggestions, but I don't think any of them are blocking -- let me know if you'd prefer to merge this as-is and I'll be happy to approve it :)
docs/GettingStarted.md
Outdated
c.tracing.test_mode.enabled = (ENV['RACK_ENV'] == 'test') | ||
end | ||
``` | ||
**If a higher precedence value is set for an option, setting that option with a lower precedence value will not change its effective value.** |
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.
One thing I added to the AgentSettingsResolver
are a few nice warnings when there are clashes between settings -- https://github.com/DataDog/dd-trace-rb/blob/master/lib/datadog/core/configuration/agent_settings_resolver.rb#L268 .
Maybe a nice feature to look into for the other options as well?
ivo.anjo@rubyshade:~/datadog/dd-trace-rb$ DD_AGENT_HOST=potato bundle exec ruby -e "require 'ddtrace'; Datadog.configure { |c| c.agent.host = 'foo' }"
W, [2023-06-20T09:47:56.429261 #64369] WARN -- ddtrace: [ddtrace] Configuration mismatch: values differ between 'c.agent.host' ("foo") and DD_AGENT_HOST environment variable ("potato"). Using "foo".
W, [2023-06-20T09:47:56.443702 #64369] WARN -- ddtrace: [ddtrace] Configuration mismatch: values differ between 'c.agent.host' ("foo") and DD_AGENT_HOST environment variable ("potato"). Using "foo".
W, [2023-06-20T09:47:56.447870 #64369] WARN -- ddtrace: [ddtrace] Configuration mismatch: values differ between 'c.agent.host' ("foo") and DD_AGENT_HOST environment variable ("potato"). Using "foo".
(The extra dupes come from things that don't use the core AgentSettingsResolver instance and recreate one instead 😭 )
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.
I implemented it as a debug log, 🙇.
REMOTE_CONFIGURATION = 2 | ||
PROGRAMMATIC = 1 | ||
DEFAULT = 0 |
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.
Minor: I'm not the biggest fan of opaque numbers (since when you inspect stuff it's very opaque), but introducing a whole new struct/class, ... may be a bit overkill. I would suggest perhaps a compromise of using a hash/array, e.g.
REMOTE_CONFIGURATION = {remote_configuration: 2}.freeze
# or
PROGRAMMATIC = {name: programmatic, level: 1}.freeze
# or
DEFAULT = [:default, 0].freeze
This doesn't need more boilerplate, and the only code that needs to change is the precedence comparison to dig inside the structure instead of using the raw number.
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.
🎶 Enum, num, num...
# | ||
# @param value [Object] the new value to be associated with this option | ||
# @param precedence [Precedence] from what precedence order this new value comes from | ||
def set(value, precedence: Precedence::PROGRAMMATIC) |
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.
Since this API is not public (...I think...?) perhaps it would make sense to not have a default for precedence:
? I think anyway you've covered all callers already :)
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.
I think the default being "the user will override anything, except Remote Configuration" is the proper default here.
I don't like that this is a public API, but hiding it also prevents users writing extensions from being able to set Precedence::DEFAULT values, which is a valid use case.
I don't think this is the perfect API, but it currently models correctly the model we are going for for remote vs local settings.
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.
Ah, I didn't mean to say we should hide it, my suggestion is to omit Precedence::PROGRAMMATIC
from being the default.
That way, we always force callers to make a decision on what precedence should be used.
# @param value [Object] the new value to be associated with this option | ||
# @param precedence [Precedence] from what precedence order this new value comes from | ||
def set(value, precedence: Precedence::PROGRAMMATIC) | ||
return @value if precedence < @precedence_set # Cannot override higher precedence value |
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.
On the topic of warnings when there are clashes between settings (comment above), I think we should at least print a warning when a setting gets ignored here.
E.g. it's very counter-intuitive that I would run a Datadog.configure { }
block 10 minutes after I started my application and my setting would get outright ignored because in the meanwhile it got set via remote configuration.
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.
Done!
module Precedence | ||
REMOTE_CONFIGURATION = 2 | ||
PROGRAMMATIC = 1 |
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.
Minor: I wonder if there should exist an internal local override mode that is the highest priority (even if we don't document it)? Even if only for testing, it seems weird that the highest priority is remote configuration and that's the latest word on the subject.
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.
That can make sense!
I don't think there's a use case for that yet, because we reset settings in between each test run.
But I'm not opposed to adding it if a use case arises.
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.
Yeah, good point, no use adding more complexity for a speculative use case.
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.
I believe this could be rewritten to:
private
attr_reader :precedence_set
I think is more idiomatic ruby
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.
I tried, but Rubocop won't let me, not even with (AllowModifiersOnSymbols: true
, which is the default) https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Style/AccessModifierDeclarations
…to-configuration Add `using_default?` method to Core::Configuration
Co-authored-by: Ivo Anjo <[email protected]>
Codecov Report
@@ Coverage Diff @@
## master #2915 +/- ##
========================================
Coverage 97.96% 97.96%
========================================
Files 1282 1282
Lines 70688 70809 +121
Branches 3249 3252 +3
========================================
+ Hits 69249 69368 +119
- Misses 1439 1441 +2
... and 1 file with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
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.
I've left a few more notes for your consideration, but I think it's in good enough shape that I'll press the magic button and let you make the decisions 👍
# | ||
# @param value [Object] the new value to be associated with this option | ||
# @param precedence [Precedence] from what precedence order this new value comes from | ||
def set(value, precedence: Precedence::PROGRAMMATIC) |
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.
Ah, I didn't mean to say we should hide it, my suggestion is to omit Precedence::PROGRAMMATIC
from being the default.
That way, we always force callers to make a decision on what precedence should be used.
if precedence[0] < @precedence_set[0] | ||
Datadog.logger.debug do | ||
"Option '#{definition.name}' not changed to '#{value}' (precedence: #{precedence[1]}) because the higher " \ | ||
"precedence value '#{@value}' (precedence: #{@precedence_set[1]}) was already set." | ||
end | ||
|
||
return @value |
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.
I'll leave the final decision up to you, but I think this should be higher than debug (at least info, possibly warn).
Setting an option is an explicit action being taken by code that the user wrote, so it seems reasonable to tell them when we're ignoring what they asked for.
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.
I was concerned with our own internal usage of Datadog.configure {}
, which the user has no control over.
These, in my mind, could trigger this log line.
But inspecting them closer, none of the internal use cases change any of the options that Remote Configuration change today, and they don't seem likely to be supported through Remote Configuration in the future.
I've bumped it up to INFO: I'm not confident WARN is the best option because Remote Configuration overriding your local configuration leaves the application in a stable state, so the WARN might not actually be of concern. I don't want to have to introduce an introspection API for users to avoid triggering the WARN ("let me check if this option was set by remote configuration before setting it locally, otherwise ddtrace will WARN").
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.
Seems reasonable!
context 'without default option' do | ||
context 'when not set' do | ||
it 'returns false' do | ||
expect(configuration.fake_test.using_default?(:without_default)).to be(false) | ||
end | ||
end | ||
|
||
context 'when not set but accessed' do | ||
it 'returns true' do | ||
configuration.fake_test.without_default | ||
expect(configuration.fake_test.using_default?(:without_default)).to be(true) | ||
end | ||
end |
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.
I believe this came from @GustavoCaso's PR, but... it's weird to me to adopt these semantics. E.g. when would it be useful to have "no default" change from false to true depending on it being accessed or not?
This seems like a potential foot-gun being armed and primed to go.
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.
That is an excellent point @ivoanjo
Maybe we should always return false
when the option hasn't been accessed or configured once. That way we always rely on the precedence value for returning the result.
def using_default?(option)
return options[option].default_precedence? if options[option]
false
end
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.
It's somewhat interesting to me that you propose that the default be false
.
So, part of my confusion on what to suggest here is that up until now I've only seen the implementation of this API, and not really what we're going to do with it.
From some our chats, I thought the intention is for using_default?
was to be used first as a way for remote configuration to decide "has the customer provided their own config directly or not?".
And in that situation, it kinda looks to me that we'd want using_default?
to be true
as a fallback when not accessed. That's because, in practice there's no default but the intention seems to be (?) that we're using this check as a proxy for did the customer set something via code or not?, and the answer to me is that they did not, and thus, it should count as true
(= using default).
But again I may be misunderstanding all of the above :)
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.
@ivoanjo, thanks for the feedback.
You are right! We should default to true
since the values haven't been yet configured or accessed, meaning the user or remote configuration hasn't been configured yet.
I thought the intention is for using_default? was to be used first as a way for remote configuration to decide "has the customer provided their own config directly or not?".
That use case, while valid, is not why I introduced the code. In AppSec we need to check if the user has configured a certain setting.
dd-trace-rb/lib/datadog/appsec/remote.rb
Lines 107 to 109 in cbbd749
def remote_features_enabled? | |
Datadog::AppSec.send(:default_setting?, :ruleset) | |
end |
And in that situation, it kinda looks to me that we'd want using_default? to be true as a fallback when not accessed.
Right!
Let me update the code to reflect the conversation above
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.
Done bb39eca
context 'with precedence' do | ||
subject(:set_option) { options_object.set_option(name, value, precedence: precedence) } | ||
let(:precedence) { 123 } | ||
|
||
it 'sets the option precedence' do | ||
set_option | ||
expect(options_object.options[name].send(:precedence_set)).to eq(precedence) | ||
end | ||
end |
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.
I wonder if we should enforce that precedence is one of the defined ones? Just to make sure nobody thinks to provide their own custom level and wreck havoc.
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.
I agree with that. We should probably validate that the precedence is part of the ones defined by us 😄
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.
I changed the test to use a real precedence value, as to not lead people into thinking it can be anything.
@marcotc makes sure to pull before doing any more work. I added a new commit changing some behaviour from |
This is part of #2848
With Remote Configuration, it will be possible to override local options remotely.
As a side effect, a decision has also been made to do not allow local options to override remote configuration, if a remote configuration is applied.
Effectively, any specific configuration option that is configured by Remote Configuration can only be reconfigured by Remote Configuration. This behavior only applies to the specific option that was changed by Remote Configuration, not the whole configuration set as a whole.
The effective configuration precedence is now:
Datadog.configure
block.#2920 has been merged into this branch. It uses precedence to check if the current option is using the default value or not.