Skip to content

Commit 196a787

Browse files
authored
@MetricOptions process based on condition (#897)
Adds support for conditionally adding metrics based on an expression
1 parent 0bc359c commit 196a787

File tree

12 files changed

+223
-11
lines changed

12 files changed

+223
-11
lines changed

micrometer-core/src/main/java/io/micronaut/configuration/metrics/annotation/MetricOptions.java

+27
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@
3636
@Retention(RUNTIME)
3737
@Target({METHOD})
3838
public @interface MetricOptions {
39+
40+
/**
41+
* Constant value to relate to annotation's tagger values.
42+
*/
43+
String MEMBER_TAGGERS = "taggers";
44+
45+
/**
46+
* Constant value to relate to annotation's filterTaggers value.
47+
*/
48+
String MEMBER_FILTER_TAGGERS = "filterTaggers";
49+
50+
/**
51+
* Constant value to relate to annotation's condition value.
52+
*/
53+
String MEMBER_CONDITION = "condition";
54+
3955
/**
4056
* @return array of {@link io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger} to apply to metrics for method.
4157
* Only utilized for filtering if {@link #filterTaggers()} is true
@@ -46,4 +62,15 @@
4662
* @return whether to filter taggers using {@link #taggers()} array
4763
*/
4864
boolean filterTaggers() default false;
65+
66+
/**
67+
* Evaluated expression that can be used to indicate whether the metric should be processed.
68+
* Will be evaluated each time the method is called, and if the condition evaluates to false the metric will not be published.
69+
* Evaluated using {@link io.micronaut.configuration.metrics.util.MetricOptionsUtil}
70+
*
71+
* @see <a href="https://docs.micronaut.io/latest/guide/#evaluatedExpressions">Evaluated Expressions</a>.
72+
* @return The condition
73+
* @since 5.10.0
74+
*/
75+
String condition() default "";
4976
}

micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/web/WebMetricsHelper.java

-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import io.micronaut.core.annotation.Internal;
2121
import io.micronaut.core.annotation.NonNull;
2222
import io.micronaut.core.annotation.Nullable;
23-
import io.micronaut.core.util.StringUtils;
2423
import io.micronaut.http.HttpAttributes;
2524
import io.micronaut.http.HttpResponse;
2625
import io.micronaut.http.HttpResponseProvider;
@@ -31,8 +30,6 @@
3130

3231
import java.util.ArrayList;
3332
import java.util.List;
34-
import java.util.Objects;
35-
import java.util.stream.Stream;
3633

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

micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/CountedInterceptor.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger;
2626
import io.micronaut.configuration.metrics.annotation.MetricOptions;
2727
import io.micronaut.configuration.metrics.annotation.RequiresMetrics;
28+
import io.micronaut.configuration.metrics.util.MetricOptionsUtil;
2829
import io.micronaut.core.annotation.AnnotationMetadata;
2930
import io.micronaut.core.annotation.Nullable;
3031
import io.micronaut.core.async.publisher.Publishers;
@@ -95,7 +96,9 @@ public CountedInterceptor(MeterRegistry meterRegistry, ConversionService convers
9596
public Object intercept(MethodInvocationContext<Object, Object> context) {
9697
final AnnotationMetadata metadata = context.getAnnotationMetadata();
9798
final String metricName = metadata.stringValue(Counted.class).orElse(DEFAULT_METRIC_NAME);
98-
if (StringUtils.isNotEmpty(metricName)) {
99+
final boolean conditionMet = MetricOptionsUtil.evaluateCondition(context);
100+
101+
if (StringUtils.isNotEmpty(metricName) && conditionMet) {
99102
InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService);
100103
try {
101104
InterceptedMethod.ResultType resultType = interceptedMethod.resultType();
@@ -151,8 +154,8 @@ public Object intercept(MethodInvocationContext<Object, Object> context) {
151154
}
152155

153156
private void doCount(AnnotationMetadata metadata, String metricName, @Nullable Throwable e, MethodInvocationContext<Object, Object> context) {
154-
List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(metadata.classValues(MetricOptions.class, "taggers"));
155-
boolean filter = metadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false);
157+
List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(metadata.classValues(MetricOptions.class, MetricOptions.MEMBER_TAGGERS));
158+
boolean filter = metadata.booleanValue(MetricOptions.class, MetricOptions.MEMBER_FILTER_TAGGERS).orElse(false);
156159
Counter.builder(metricName)
157160
.tags(
158161
methodTaggers.isEmpty() ? Collections.emptyList() :

micrometer-core/src/main/java/io/micronaut/configuration/metrics/micrometer/intercept/TimedInterceptor.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.micronaut.configuration.metrics.aggregator.AbstractMethodTagger;
2828
import io.micronaut.configuration.metrics.annotation.MetricOptions;
2929
import io.micronaut.configuration.metrics.annotation.RequiresMetrics;
30+
import io.micronaut.configuration.metrics.util.MetricOptionsUtil;
3031
import io.micronaut.core.annotation.AnnotationMetadata;
3132
import io.micronaut.core.annotation.AnnotationValue;
3233
import io.micronaut.core.annotation.TypeHint;
@@ -123,10 +124,12 @@ protected TimedInterceptor(MeterRegistry meterRegistry, ConversionService conver
123124
public Object intercept(MethodInvocationContext<Object, Object> context) {
124125
final AnnotationMetadata metadata = context.getAnnotationMetadata();
125126
final AnnotationValue<TimedSet> timedSet = metadata.getAnnotation(TimedSet.class);
126-
if (timedSet != null) {
127+
final boolean conditionMet = MetricOptionsUtil.evaluateCondition(context);
128+
129+
if (timedSet != null && conditionMet) {
127130
final List<AnnotationValue<Timed>> timedAnnotations = timedSet.getAnnotations(VALUE_MEMBER, Timed.class);
128-
if (!timedAnnotations.isEmpty()) {
129131

132+
if (!timedAnnotations.isEmpty()) {
130133
String exceptionClass = "none";
131134
List<Timer.Sample> syncInvokeSamples = null;
132135
InterceptedMethod interceptedMethod = InterceptedMethod.of(context, conversionService);
@@ -216,8 +219,8 @@ private void stopTimed(String metricName, Timer.Sample sample,
216219
final String description = metadata.stringValue("description").orElse(null);
217220
final String[] tags = metadata.stringValues("extraTags");
218221
final AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
219-
final List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(annotationMetadata.classValues(MetricOptions.class, "taggers"));
220-
final boolean filter = annotationMetadata.booleanValue(MetricOptions.class, "filterTaggers").orElse(false);
222+
final List<Class<? extends AbstractMethodTagger>> taggers = Arrays.asList(annotationMetadata.classValues(MetricOptions.class, MetricOptions.MEMBER_TAGGERS));
223+
final boolean filter = annotationMetadata.booleanValue(MetricOptions.class, MetricOptions.MEMBER_FILTER_TAGGERS).orElse(false);
221224
final double[] percentiles = metadata.doubleValues("percentiles");
222225
final boolean histogram = metadata.isTrue("histogram");
223226
final Timer timer = Timer.builder(metricName)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2017-2024 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.configuration.metrics.util;
17+
18+
import io.micronaut.aop.MethodInvocationContext;
19+
import io.micronaut.configuration.metrics.annotation.MetricOptions;
20+
import io.micronaut.core.expressions.EvaluatedExpression;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
/**
25+
* Utility class for {@link MetricOptions}. Sharing means of evaluating condition
26+
*
27+
* @since 5.10.0
28+
* @author Haiden Rothwell
29+
*/
30+
final public class MetricOptionsUtil {
31+
32+
private static final Logger LOG = LoggerFactory.getLogger(MetricOptionsUtil.class);
33+
34+
/**
35+
* Evaluates the condition ({@link EvaluatedExpression}) contained within the {@link MethodInvocationContext}'s {@link MetricOptions} annotation.
36+
* If no condition is present, the default value of true is returned.
37+
*
38+
* @param context {@link MethodInvocationContext} to evaluate for
39+
* @return condition's result
40+
*/
41+
public static boolean evaluateCondition(MethodInvocationContext<?, ?> context) {
42+
if (!context.isPresent(MetricOptions.class, MetricOptions.MEMBER_CONDITION)) {
43+
return true;
44+
}
45+
boolean expressionResult = context.booleanValue(MetricOptions.class, MetricOptions.MEMBER_CONDITION).orElse(false);
46+
if (LOG.isDebugEnabled()) {
47+
LOG.debug("MetricOptions condition evaluated to {} for invocation: {}", expressionResult, context);
48+
}
49+
return expressionResult;
50+
}
51+
}

micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/CountedAnnotationSpec.groovy

+36-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class CountedAnnotationSpec extends Specification {
140140

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

145145
then:
146146
thrown(MeterNotFoundException)
@@ -155,4 +155,39 @@ class CountedAnnotationSpec extends Specification {
155155
cleanup:
156156
ctx.close()
157157
}
158+
159+
void "metric is generated when condition evaluates to true"(){
160+
given:
161+
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": true])
162+
CountedTarget cc = ctx.getBean(CountedTarget)
163+
MeterRegistry registry = ctx.getBean(MeterRegistry)
164+
165+
when:
166+
Integer result = cc.maxWithCondition(4, 10)
167+
def counter = registry.get("counted.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").counter()
168+
169+
then:
170+
result == 10
171+
counter.count() == 1
172+
173+
cleanup:
174+
ctx.close()
175+
}
176+
177+
void "metric is not generated when condition evaluates to false"(){
178+
given:
179+
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": false])
180+
CountedTarget cc = ctx.getBean(CountedTarget)
181+
MeterRegistry registry = ctx.getBean(MeterRegistry)
182+
183+
when:
184+
Integer result = cc.maxWithCondition(4, 10)
185+
registry.get("counted.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").counter()
186+
187+
then:
188+
thrown(MeterNotFoundException)
189+
190+
cleanup:
191+
ctx.close()
192+
}
158193
}

micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/annotation/TimeAnnotationSpec.groovy

+36
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,40 @@ class TimeAnnotationSpec extends Specification {
169169
cleanup:
170170
ctx.close()
171171
}
172+
173+
void "metric is generated when condition evaluates to true"(){
174+
given:
175+
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": true])
176+
TimedTarget tt = ctx.getBean(TimedTarget)
177+
MeterRegistry registry = ctx.getBean(MeterRegistry)
178+
179+
when:
180+
Integer result = tt.maxWithCondition(4, 10)
181+
def timer = registry.get("timed.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").timer()
182+
183+
then:
184+
result == 10
185+
timer.count() == 1
186+
timer.totalTime(MILLISECONDS) > 0
187+
188+
cleanup:
189+
ctx.close()
190+
}
191+
192+
void "metric is not generated when condition evaluates to false"(){
193+
given:
194+
ApplicationContext ctx = ApplicationContext.run(["test.properties.enabled": false])
195+
TimedTarget tt = ctx.getBean(TimedTarget)
196+
MeterRegistry registry = ctx.getBean(MeterRegistry)
197+
198+
when:
199+
Integer result = tt.maxWithCondition(4, 10)
200+
registry.get("timed.test.maxWithCondition.blocking").tags("ordered", "2", "parameters", "a b").timer()
201+
202+
then:
203+
thrown(MeterNotFoundException)
204+
205+
cleanup:
206+
ctx.close()
207+
}
172208
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.micronaut.docs;
2+
3+
import io.micrometer.core.annotation.Timed;
4+
import io.micronaut.configuration.metrics.annotation.MetricOptions;
5+
import jakarta.inject.Singleton;
6+
7+
@Singleton
8+
public class MetricOptionsConditionExample {
9+
10+
@MetricOptions(
11+
// If condition is set, the metric will only be processed and published when it evaluates to true
12+
condition = " #{ env['property'] == true }"
13+
)
14+
@Timed(value = "do_something")
15+
public void doSomething() {
16+
// ...
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.micronaut.docs;
2+
3+
import io.micrometer.core.annotation.Timed;
4+
import io.micronaut.configuration.metrics.annotation.MetricOptions;
5+
import jakarta.inject.Singleton;
6+
7+
@Singleton
8+
public class MetricOptionsFilterTaggersExample {
9+
10+
@MetricOptions(
11+
filterTaggers = true, // Specify that not all taggers should be applied
12+
taggers = {MethodNameTagger.class} // Specific taggers to apply
13+
)
14+
@Timed(value = "do_something")
15+
public void doSomething() {
16+
// ...
17+
}
18+
}

micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/CountedTarget.java

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Integer maxWithOptions(int a, int b) {
2727
return Math.max(a, b);
2828
}
2929

30+
@Counted("counted.test.maxWithCondition.blocking")
31+
@MetricOptions(condition = "#{ env['test.properties.enabled'] }")
32+
Integer maxWithCondition(int a, int b) { return Math.max(a,b); }
33+
3034
@Counted("counted.test.max.blocking")
3135
Integer error(int a, int b) {
3236
throw new NumberFormatException("cannot");

micrometer-core/src/test/java/io/micronaut/configuration/metrics/annotation/TimedTarget.java

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ Integer maxWithOptions(int a, int b) {
2828
return Math.max(a, b);
2929
}
3030

31+
@Timed("timed.test.maxWithCondition.blocking")
32+
@MetricOptions(condition = "#{ env['test.properties.enabled'] }")
33+
Integer maxWithCondition(int a, int b) { return Math.max(a,b); }
34+
3135
@Timed("timed.test.repeated1")
3236
@Timed("timed.test.repeated2")
3337
Integer repeated(int a, int b) {

src/main/docs/guide/metricsAnnotations.adoc

+16
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,19 @@ In order to support adding additional tags programmatically similar to Micromete
1010
----
1111
include::{testsmetricscore}/MethodNameTagger.java[]
1212
----
13+
14+
You can filter these taggers by utilizing the `MetricOptions` annotation
15+
16+
.MetricOptions filtering taggers example
17+
[source,java]
18+
----
19+
include::{testmetricscore}/MetricOptionsFilterTaggersExample.java[]
20+
----
21+
22+
The `MetricOptions` annotation also provides a means of only processing / publishing a metric based on an `EvaluatedExpression`
23+
24+
.MetricOptions condition example
25+
[source,java]
26+
----
27+
include::{testmetricscore}/MetricOptionsConditionExample.java[]
28+
----

0 commit comments

Comments
 (0)