Skip to content
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

Merged
merged 14 commits into from
Jun 27, 2023
Merged

Add support for Option precedence #2915

merged 14 commits into from
Jun 27, 2023

Conversation

marcotc
Copy link
Member

@marcotc marcotc commented Jun 19, 2023

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:

  1. Remote Configuration.
  2. Custom options inside a Datadog.configure block.
  3. Environment variables.

#2920 has been merged into this branch. It uses precedence to check if the current option is using the default value or not.

@marcotc marcotc requested a review from a team June 19, 2023 21:37
@marcotc marcotc self-assigned this Jun 19, 2023
@github-actions github-actions bot added the core Involves Datadog core libraries label Jun 19, 2023
Copy link
Member

@ivoanjo ivoanjo left a 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 Show resolved Hide resolved
docs/GettingStarted.md Outdated Show resolved Hide resolved
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.**
Copy link
Member

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 😭 )

Copy link
Member Author

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, 🙇.

docs/GettingStarted.md Outdated Show resolved Hide resolved
Comment on lines 14 to 16
REMOTE_CONFIGURATION = 2
PROGRAMMATIC = 1
DEFAULT = 0
Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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 :)

Copy link
Member Author

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.

Copy link
Member

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.

lib/datadog/core/configuration/option.rb Outdated Show resolved Hide resolved
# @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
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines 13 to 15
module Precedence
REMOTE_CONFIGURATION = 2
PROGRAMMATIC = 1
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

docs/GettingStarted.md Show resolved Hide resolved
Comment on lines +89 to +90
attr_reader :precedence_set
private :precedence_set
Copy link
Member

@GustavoCaso GustavoCaso Jun 20, 2023

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

Copy link
Member Author

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

@marcotc marcotc changed the base branch from tracing-remote-configuration to master June 26, 2023 21:49
@codecov-commenter
Copy link

codecov-commenter commented Jun 26, 2023

Codecov Report

Merging #2915 (c0f8a62) into master (a4b81cf) will increase coverage by 0.00%.
The diff coverage is 100.00%.

@@           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     
Impacted Files Coverage Δ
lib/datadog/core/configuration/option.rb 98.00% <100.00%> (+0.85%) ⬆️
lib/datadog/core/configuration/options.rb 100.00% <100.00%> (ø)
spec/datadog/core/configuration/base_spec.rb 100.00% <100.00%> (ø)
spec/datadog/core/configuration/option_spec.rb 99.48% <100.00%> (+0.18%) ⬆️
spec/datadog/core/configuration/options_spec.rb 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Copy link
Member

@ivoanjo ivoanjo left a 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 👍

docs/GettingStarted.md Outdated Show resolved Hide resolved
docs/GettingStarted.md Outdated Show resolved Hide resolved
#
# @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)
Copy link
Member

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.

Comment on lines 36 to 42
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
Copy link
Member

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.

Copy link
Member Author

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").

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable!

lib/datadog/core/configuration/option.rb Outdated Show resolved Hide resolved
Comment on lines 188 to 200
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
Copy link
Member

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.

Copy link
Member

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

@ivoanjo @marcotc any thoughts?

Copy link
Member

@ivoanjo ivoanjo Jun 27, 2023

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 :)

Copy link
Member

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.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done bb39eca

Comment on lines 125 to 133
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
Copy link
Member

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.

Copy link
Member

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 😄

Copy link
Member Author

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.

@GustavoCaso
Copy link
Member

@marcotc makes sure to pull before doing any more work. I added a new commit changing some behaviour from using_default?

@marcotc marcotc merged commit 7b9014b into master Jun 27, 2023
@marcotc marcotc deleted the precedence branch June 27, 2023 22:03
@github-actions github-actions bot added this to the 1.13.0 milestone Jun 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Involves Datadog core libraries
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants