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

@MetricOptions process based on condition #897

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@
@Retention(RUNTIME)
@Target({METHOD})
public @interface MetricOptions {

/**
* Constant value to relate to annotation's tagger values.
*/
String MEMBER_TAGGERS = "taggers";

/**
* Constant value to relate to annotation's filterTaggers value.
*/
String MEMBER_FILTER_TAGGERS = "filterTaggers";

/**
* Constant value to relate to annotation's condition value.
*/
String MEMBER_CONDITION = "condition";

/**
* @return array of {@link io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger} to apply to metrics for method.
* Only utilized for filtering if {@link #filterTaggers()} is true
Expand All @@ -46,4 +62,15 @@
* @return whether to filter taggers using {@link #taggers()} array
*/
boolean filterTaggers() default false;

/**
* Evaluated expression that can be used to indicate whether the metric should be processed.
* Will be evaluated each time the method is called, and if the condition evaluates to false the metric will not be published.
* Evaluated using {@link io.micronaut.configuration.metrics.util.MetricOptionsUtil}
*
* @see <a href="https://docs.micronaut.io/latest/guide/#evaluatedExpressions">Evaluated Expressions</a>.
* @return The condition
* @since 5.10.0
*/
String condition() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpAttributes;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpResponseProvider;
Expand All @@ -31,8 +30,6 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger;
import io.micronaut.configuration.metrics.annotation.MetricOptions;
import io.micronaut.configuration.metrics.annotation.RequiresMetrics;
import io.micronaut.configuration.metrics.util.MetricOptionsUtil;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.async.publisher.Publishers;
Expand Down Expand Up @@ -95,7 +96,9 @@ public CountedInterceptor(MeterRegistry meterRegistry, ConversionService convers
public Object intercept(MethodInvocationContext<Object, Object> context) {
final AnnotationMetadata metadata = context.getAnnotationMetadata();
final String metricName = metadata.stringValue(Counted.class).orElse(DEFAULT_METRIC_NAME);
if (StringUtils.isNotEmpty(metricName)) {
final boolean conditionMet = MetricOptionsUtil.evaluateCondition(context);

if (StringUtils.isNotEmpty(metricName) && conditionMet) {
InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService);
try {
InterceptedMethod.ResultType resultType = interceptedMethod.resultType();
Expand Down Expand Up @@ -151,8 +154,8 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
}

private void doCount(AnnotationMetadata metadata, String metricName, @Nullable Throwable e, MethodInvocationContext<Object, Object> context) {
List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(metadata.classValues(MetricOptions.class, "taggers"));
boolean filter = metadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false);
List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(metadata.classValues(MetricOptions.class, MetricOptions.MEMBER_TAGGERS));
boolean filter = metadata.booleanValue(MetricOptions.class, MetricOptions.MEMBER_FILTER_TAGGERS).orElse(false);
Counter.builder(metricName)
.tags(
methodTaggers.isEmpty() ? Collections.emptyList() :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger;
import io.micronaut.configuration.metrics.annotation.MetricOptions;
import io.micronaut.configuration.metrics.annotation.RequiresMetrics;
import io.micronaut.configuration.metrics.util.MetricOptionsUtil;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.TypeHint;
Expand Down Expand Up @@ -123,10 +124,12 @@ protected TimedInterceptor(MeterRegistry meterRegistry, ConversionService conver
public Object intercept(MethodInvocationContext<Object, Object> context) {
final AnnotationMetadata metadata = context.getAnnotationMetadata();
final AnnotationValue<TimedSet> timedSet = metadata.getAnnotation(TimedSet.class);
if (timedSet != null) {
final boolean conditionMet = MetricOptionsUtil.evaluateCondition(context);

if (timedSet != null && conditionMet) {
final List<AnnotationValue<Timed>> timedAnnotations = timedSet.getAnnotations(VALUE_MEMBER, Timed.class);
if (!timedAnnotations.isEmpty()) {

if (!timedAnnotations.isEmpty()) {
String exceptionClass = "none";
List<Timer.Sample> syncInvokeSamples = null;
InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService);
Expand Down Expand Up @@ -216,8 +219,8 @@ private void stopTimed(String metricName, Timer.Sample sample,
final String description = metadata.stringValue("description").orElse(null);
final String[] tags = metadata.stringValues("extraTags");
final AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
final List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(annotationMetadata.classValues(MetricOptions.class, "taggers"));
final boolean filter = annotationMetadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false);
final List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(annotationMetadata.classValues(MetricOptions.class, MetricOptions.MEMBER_TAGGERS));
final boolean filter = annotationMetadata.booleanValue(MetricOptions.class, MetricOptions.MEMBER_FILTER_TAGGERS).orElse(false);
final double[] percentiles = metadata.doubleValues("percentiles");
final boolean histogram = metadata.isTrue("histogram");
final Timer timer = Timer.builder(metricName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.configuration.metrics.util;

import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.configuration.metrics.annotation.MetricOptions;
import io.micronaut.core.expressions.EvaluatedExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utility class for {@link MetricOptions}. Sharing means of evaluating condition
*
* @since 5.10.0
* @author Haiden Rothwell
*/
final public class MetricOptionsUtil {

private static final Logger LOG = LoggerFactory.getLogger(MetricOptionsUtil.class);

/**
* Evaluates the condition ({@link EvaluatedExpression}) contained within the {@link MethodInvocationContext}'s {@link MetricOptions} annotation.
* If no condition is present, the default value of true is returned.
*
* @param context {@link MethodInvocationContext} to evaluate for
* @return condition's result
*/
public static boolean evaluateCondition(MethodInvocationContext<?, ?> context) {
Copy link
Contributor

Choose a reason for hiding this comment

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

add javadoc and make not public

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar situation as the whole class being not public: if public is removed it is unable to be called where it currently is

if (!context.isPresent(MetricOptions.class, MetricOptions.MEMBER_CONDITION)) {
return true;
}
boolean expressionResult = context.booleanValue(MetricOptions.class, MetricOptions.MEMBER_CONDITION).orElse(false);
if (LOG.isDebugEnabled()) {
LOG.debug("MetricOptions condition evaluated to {} for invocation: {}", expressionResult, context);
}
return expressionResult;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class CountedAnnotationSpec extends Specification {

when:
Integer result = cc.max(4, 10)
registry.get("counted.test.max.blocking").tags("ordered", "1", "parameters", "a b").timer()
registry.get("counted.test.max.blocking").tags("ordered", "1", "parameters", "a b").counter()

then:
thrown(MeterNotFoundException)
Expand All @@ -155,4 +155,39 @@ class CountedAnnotationSpec extends Specification {
cleanup:
ctx.close()
}

void "metric is generated when condition evaluates to true"(){
given:
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": true])
CountedTarget cc = ctx.getBean(CountedTarget)
MeterRegistry registry = ctx.getBean(MeterRegistry)

when:
Integer result = cc.maxWithCondition(4, 10)
def counter = registry.get("counted.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").counter()

then:
result == 10
counter.count() == 1

cleanup:
ctx.close()
}

void "metric is not generated when condition evaluates to false"(){
given:
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": false])
CountedTarget cc = ctx.getBean(CountedTarget)
MeterRegistry registry = ctx.getBean(MeterRegistry)

when:
Integer result = cc.maxWithCondition(4, 10)
registry.get("counted.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").counter()

then:
thrown(MeterNotFoundException)

cleanup:
ctx.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,40 @@ class TimeAnnotationSpec extends Specification {
cleanup:
ctx.close()
}

void "metric is generated when condition evaluates to true"(){
given:
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": true])
TimedTarget tt = ctx.getBean(TimedTarget)
MeterRegistry registry = ctx.getBean(MeterRegistry)

when:
Integer result = tt.maxWithCondition(4, 10)
def timer = registry.get("timed.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").timer()

then:
result == 10
timer.count() == 1
timer.totalTime(MILLISECONDS) > 0

cleanup:
ctx.close()
}

void "metric is not generated when condition evaluates to false"(){
given:
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": false])
TimedTarget tt = ctx.getBean(TimedTarget)
MeterRegistry registry = ctx.getBean(MeterRegistry)

when:
Integer result = tt.maxWithCondition(4, 10)
registry.get("timed.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").timer()

then:
thrown(MeterNotFoundException)

cleanup:
ctx.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.micronaut.docs;

import io.micrometer.core.annotation.Timed;
import io.micronaut.configuration.metrics.annotation.MetricOptions;
import jakarta.inject.Singleton;

@Singleton
public class MetricOptionsConditionExample {

@MetricOptions(
// If condition is set, the metric will only be processed and published when it evaluates to true
condition = " #{ env['property'] == true }"
)
@Timed(value = "do_something")
public void doSomething() {
// ...
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.micronaut.docs;

import io.micrometer.core.annotation.Timed;
import io.micronaut.configuration.metrics.annotation.MetricOptions;
import jakarta.inject.Singleton;

@Singleton
public class MetricOptionsFilterTaggersExample {

@MetricOptions(
filterTaggers = true, // Specify that not all taggers should be applied
taggers = {MethodNameTagger.class} // Specific taggers to apply
)
@Timed(value = "do_something")
public void doSomething() {
// ...
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Integer maxWithOptions(int a, int b) {
return Math.max(a, b);
}

@Counted("counted.test.maxWithCondition.blocking")
@MetricOptions(condition = "#{ env['test.properties.enabled'] }")
Integer maxWithCondition(int a, int b) { return Math.max(a,b); }

@Counted("counted.test.max.blocking")
Integer error(int a, int b) {
throw new NumberFormatException("cannot");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ Integer maxWithOptions(int a, int b) {
return Math.max(a, b);
}

@Timed("timed.test.maxWithCondition.blocking")
@MetricOptions(condition = "#{ env['test.properties.enabled'] }")
Integer maxWithCondition(int a, int b) { return Math.max(a,b); }

@Timed("timed.test.repeated1")
@Timed("timed.test.repeated2")
Integer repeated(int a, int b) {
Expand Down
16 changes: 16 additions & 0 deletions src/main/docs/guide/metricsAnnotations.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,19 @@ In order to support adding additional tags programmatically similar to Micromete
----
include::{testsmetricscore}/MethodNameTagger.java[]
----

You can filter these taggers by utilizing the `MetricOptions` annotation

.MetricOptions filtering taggers example
[source,java]
----
include::{testmetricscore}/MetricOptionsFilterTaggersExample.java[]
----

The `MetricOptions` annotation also provides a means of only processing / publishing a metric based on an `EvaluatedExpression`

.MetricOptions condition example
[source,java]
----
include::{testmetricscore}/MetricOptionsConditionExample.java[]
----
Loading