-
Notifications
You must be signed in to change notification settings - Fork 1k
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
Take Supplier<Context> instead of Context #3443
Take Supplier<Context> instead of Context #3443
Conversation
This allows avoiding instantiation of a Context object in the case that the ObservationRegistry is a no-op. This does not overload the methods by having a variant that takes a Context and one that takes a Supplier because this becomes ambiguous for the compiler in some cases. A lambda can be used in cases where you have a Context object to pass. Alternatively, you can implement Supplier in your custom Context class to avoid needing a lambda.
context = contextSupplier.get(); | ||
observation = Observation.start(convention, defaultConvention, context, observationRegistry); | ||
observation = Observation.start(convention, defaultConvention, contextSupplier, observationRegistry); | ||
context = (T) observation.getContext(); |
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.
Switched the order here so the supplier isn't called twice.
public class OkHttpContext extends RequestReplySenderContext<Request.Builder, Response> { | ||
@SuppressWarnings("jol") | ||
public class OkHttpContext extends RequestReplySenderContext<Request.Builder, Response> | ||
implements Supplier<OkHttpContext> { |
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.
Demonstrates one way to deal with this breaking change for users. Then they can pass the, in this case, OkHttpContext instead of using a lambda.
ObservationConvention<T> convention; | ||
if (customConvention != null) { | ||
convention = customConvention; | ||
} | ||
else { | ||
convention = registry.observationConfig().getObservationConvention(context, defaultConvention); | ||
convention = registry.observationConfig().getObservationConvention(contextSupplier.get(), |
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 will result in the supplier being called twice, which feels less than ideal.
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.
Can we review the noop checks for the whole class, probably refactor it into a method (or two) and do the registry based check early?
After we passed the registry based check, we can create the context once and pass it to the next method (a private overload without the supplier)?
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 issue is interfaces can't have private methods. But regardless of that, it's a little hard to centralize this much more than it is, I think, due to the variety of factory methods and the different logic they have. Suggestions welcome, though.
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.
What if we create a dedicated factory/builder class instead of default/static factory methods? The class would centralize noop checks for all variants of observation creations if possible.
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.
Looking at this more, we have two constructors for SimpleObservation, so I think we're going to have at least two places in factory methods we need to check for a no-op registry. It turns out we end up with 3 now because there is the option to make an Observation with a name, with a convention, or to have the convention looked up. The convention look-up requires the context, so we need to get the context already at that point. I've refactored this method to not call one of the other methods and instead instantiate the SimpleObservation itself, which avoids supplying the context twice. It leads to a little code duplication, but the look-up of the convention in the registry is a notable difference that happens in the middle of the logic.
On a dedicated factory/builder class, that's an option. I worry it's a bigger change and we're basically out of time to iterate on and get feedback on such things. I don't know that it would change a lot. The no-op check that can be centralized I think is just calling registry.isNoop()
and separately !registry.observationConfig().isObservationEnabled(name, context)
. This is already only done in the 3 places I mentioned above because they are end up doing different things.
A tangential thing is, we're not consistent on whether we accept a @Nullable ObservationRegistry
or not in the factory methods. We can look at that separately, I think, but we should make that consistent unless there's some reason to be different.
...meter-observation/src/main/java/io/micrometer/observation/docs/ObservationDocumentation.java
Outdated
Show resolved
Hide resolved
* @param registry observation registry | ||
* @return observation | ||
*/ | ||
default <T extends Observation.Context> Observation observation(@Nullable ObservationConvention<T> customConvention, | ||
@NonNull ObservationConvention<T> defaultConvention, @NonNull T context, | ||
@NonNull ObservationRegistry registry) { |
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.
Unrelated to this change, but I removed the @NonNull
annotations because they are redundant. We annotate the whole package with @NonNullApi
.
...a/io/micrometer/core/instrument/observation/ObservationOrTimerCompatibleInstrumentation.java
Show resolved
Hide resolved
|
@@ -59,19 +59,20 @@ public interface Observation extends ObservationView { | |||
* @return started observation | |||
*/ | |||
static Observation start(String name, ObservationRegistry registry) { | |||
return start(name, null, registry); | |||
return start(name, () -> null, registry); |
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.
Should we provide a null context supplier so users can do something like this: start(name, Context.NULL, registry)
?
Though if they want to pass null, they can just call the overload where this param is missing.
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, I'm not sure what is best. It would seem a bit silly to provide a null supplier, which is just going to result in us eventually instantiating a Context
object, rather than providing a default context supplier that returns a new Context()
.
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 adding static supplier field to Context
, but it doesn't really save anything compared to using Context::new
instead, so I think I'll leave it out until/unless we get feedback from users. I'll switch these () -> null
to Context::new
instead.
ObservationConvention<T> convention; | ||
if (customConvention != null) { | ||
convention = customConvention; | ||
} | ||
else { | ||
convention = registry.observationConfig().getObservationConvention(context, defaultConvention); | ||
convention = registry.observationConfig().getObservationConvention(contextSupplier.get(), |
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.
Can we review the noop checks for the whole class, probably refactor it into a method (or two) and do the registry based check early?
After we passed the registry based check, we can create the context once and pass it to the next method (a private overload without the supplier)?
...meter-observation/src/main/java/io/micrometer/observation/docs/ObservationDocumentation.java
Outdated
Show resolved
Hide resolved
assertThat(Observation.createNotStarted("test.timer", null)).isSameAs(NOOP); | ||
assertThat(Observation.createNotStarted("test.timer", new Observation.Context(), null)).isSameAs(NOOP); | ||
assertThat(Observation.createNotStarted("test.timer", Observation.Context::new, null)).isSameAs(NOOP); |
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 quite like this actually. :D
@@ -146,21 +153,21 @@ static <T extends Context> Observation createNotStarted(@Nullable ObservationCon | |||
*/ | |||
static Observation start(ObservationConvention<? extends Context> observationConvention, | |||
@Nullable ObservationRegistry registry) { | |||
return start(observationConvention, null, registry); | |||
return start(observationConvention, () -> null, registry); |
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 couldn't get rid of this () -> null
lambda because of generics. I wonder if we need this method. What's the use case to provide an ObservationConvention
that doesn't target Context
and not provide a context? If it only ever targets Context
, then we don't need the ? extends Context
and then we can change the lambda to Context::new
.
So unless I'm missing the use case, I think we should either delete this method, or change the first parameter type to ObservationConvention<Context> observationConvention
. Thoughts?
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 update the parameter type to ObservationConvention<Context>
since having another Context type when a Context
will be created doesn't make sense.
When not providing a context, one of type `Context` will be made. It doesn't make sense to take an `ObservationConvention` targeting a more specific type, then.
This allows avoiding instantiation of a Context object in the case that the ObservationRegistry is a no-op. This does not overload the methods by having a variant that takes a Context and one that takes a Supplier because this becomes ambiguous for the compiler in some cases (reported to us when we added one such method to
ObservationDocumentation
previously). A lambda can be used in cases where you have a Context object to pass. Alternatively, you can implement Supplier in your custom Context class to avoid needing a lambda.Migration guide
Anywhere you were passing a
Context
object to anObservation
factory method (static methods onObservation
and instance methods onObservationDocumentation
), the parameter type has been changed toSupplier<Context>
to allow for lazy instantiation of the context. There are a few options available on how to migrate your code.Lambda
Using a lambda is one way to adapt code to the new API. Before where you may have had code like the following:
It can be updated to:
Method reference
If you do not need to pass arguments to the context, you can use a method reference to the constructor.
Make your context a supplier
Another alternative if you are using a custom context is to make it implement
Supplier
and then you can pass the context object as you were before. For example, anOkHttpContext
class could be updated to the following.And then used the same as before, such as: