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

Take Supplier<Context> instead of Context #3443

Merged
merged 7 commits into from
Sep 30, 2022

Conversation

shakuzen
Copy link
Member

@shakuzen shakuzen commented Sep 29, 2022

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 an Observation factory method (static methods on Observation and instance methods on ObservationDocumentation), the parameter type has been changed to Supplier<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:

JerseyContext jerseyContext = new JerseyContext(event);
Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.jerseyObservationConvention,
                        new DefaultJerseyObservationConvention(this.metricName), jerseyContext, this.registry);

It can be updated to:

JerseyContext jerseyContext = new JerseyContext(event);
Observation observation = JerseyObservationDocumentation.DEFAULT.start(this.jerseyObservationConvention,
                        new DefaultJerseyObservationConvention(this.metricName), () -> jerseyContext, this.registry);

Method reference

If you do not need to pass arguments to the context, you can use a method reference to the constructor.

Observation.start("my.observation", Context::new, observationRegistry)

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, an OkHttpContext class could be updated to the following.

public class OkHttpContext extends RequestReplySenderContext<Request.Builder, Response>
        implements Supplier<OkHttpContext> {
    // ...
    @Override
    public OkHttpContext get() {
        return this;
    }

And then used the same as before, such as:

Observation observation = OkHttpObservationDocumentation.DEFAULT.observation(this.observationConvention,
        new DefaultOkHttpObservationConvention(requestMetricName), okHttpContext, this.registry).start();

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

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> {
Copy link
Member Author

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

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.

Copy link
Member

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

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

* @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) {
Copy link
Member Author

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.

@sonatype-lift
Copy link
Contributor

sonatype-lift bot commented Sep 29, 2022

⚠️ 10 God Classes were detected by Lift in this project. Visit the Lift web console for more details.

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

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.

Copy link
Member Author

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

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

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

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

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

@shakuzen shakuzen Sep 30, 2022

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?

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'll update the parameter type to ObservationConvention<Context> since having another Context type when a Context will be created doesn't make sense.

@shakuzen shakuzen changed the title WIP Take Supplier<Context> instead of Context Take Supplier<Context> instead of Context Sep 30, 2022
@shakuzen shakuzen marked this pull request as ready for review September 30, 2022 03:39
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.
@marcingrzejszczak marcingrzejszczak merged commit d574172 into micrometer-metrics:main Sep 30, 2022
@shakuzen shakuzen deleted the context-supplier branch October 3, 2022 02:43
izeye added a commit to izeye/micrometer that referenced this pull request Oct 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants