From 10614998cf0779b3d6bbd33a822b96a10695572e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Serralheiro?= Date: Tue, 9 Jul 2024 09:04:36 +0100 Subject: [PATCH] Service level objectives support on `@Timed` annotation (#5145) Adding the ability to specify, on @Timed annotations, the service level objectives. Using seconds as the unit of time for these values. --- .../io/micrometer/core/annotation/Timed.java | 9 ++++++ .../io/micrometer/core/aop/TimedAspect.java | 10 ++++++- .../io/micrometer/core/instrument/Timer.java | 7 ++++- .../micrometer/core/aop/TimedAspectTest.java | 30 +++++++++++++++++++ .../MetricsRequestEventListenerTimedTest.java | 15 ++++++++++ .../server/resources/TimedResource.java | 7 +++++ 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/micrometer-core/src/main/java/io/micrometer/core/annotation/Timed.java b/micrometer-core/src/main/java/io/micrometer/core/annotation/Timed.java index da75034002..fc62d882a3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/annotation/Timed.java +++ b/micrometer-core/src/main/java/io/micrometer/core/annotation/Timed.java @@ -74,6 +74,15 @@ */ boolean histogram() default false; + /** + * List of service level objectives to calculate client-side for the + * {@link io.micrometer.core.instrument.Timer} in seconds. For example, for a 100ms + * should be passed as {@code 0.1}. + * @return service level objectives to calculate + * @see io.micrometer.core.instrument.Timer.Builder#serviceLevelObjectives(java.time.Duration...) + */ + double[] serviceLevelObjectives() default {}; + /** * Description of the {@link io.micrometer.core.instrument.Timer}. * @return meter description diff --git a/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java b/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java index 313da42f53..cc3dd33d56 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java +++ b/micrometer-core/src/main/java/io/micrometer/core/aop/TimedAspect.java @@ -20,14 +20,18 @@ import io.micrometer.core.annotation.Incubating; import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.util.TimeUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; +import java.time.Duration; +import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; @@ -254,7 +258,11 @@ private Timer.Builder recordBuilder(ProceedingJoinPoint pjp, Timed timed, String .tags(EXCEPTION_TAG, exceptionClass) .tags(tagsBasedOnJoinPoint.apply(pjp)) .publishPercentileHistogram(timed.histogram()) - .publishPercentiles(timed.percentiles().length == 0 ? null : timed.percentiles()); + .serviceLevelObjectives( + timed.serviceLevelObjectives().length > 0 ? Arrays.stream(timed.serviceLevelObjectives()) + .mapToObj(s -> Duration.ofNanos((long) TimeUtils.secondsToUnit(s, TimeUnit.NANOSECONDS))) + .toArray(Duration[]::new) : null); + if (meterTagAnnotationHandler != null) { meterTagAnnotationHandler.addAnnotatedParameters(builder, pjp); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Timer.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Timer.java index f7e8e475b9..02c2d6c4d8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Timer.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Timer.java @@ -22,6 +22,7 @@ import io.micrometer.core.instrument.distribution.HistogramSupport; import io.micrometer.core.instrument.distribution.ValueAtPercentile; import io.micrometer.core.instrument.distribution.pause.PauseDetector; +import io.micrometer.core.instrument.util.TimeUtils; import java.time.Duration; import java.util.Arrays; @@ -100,7 +101,11 @@ static Builder builder(Timed timed, String defaultName) { return new Builder(timed.value().isEmpty() ? defaultName : timed.value()).tags(timed.extraTags()) .description(timed.description().isEmpty() ? null : timed.description()) .publishPercentileHistogram(timed.histogram()) - .publishPercentiles(timed.percentiles().length > 0 ? timed.percentiles() : null); + .publishPercentiles(timed.percentiles().length > 0 ? timed.percentiles() : null) + .serviceLevelObjectives( + timed.serviceLevelObjectives().length > 0 ? Arrays.stream(timed.serviceLevelObjectives()) + .mapToObj(s -> Duration.ofNanos((long) TimeUtils.secondsToUnit(s, TimeUnit.NANOSECONDS))) + .toArray(Duration[]::new) : null); } /** diff --git a/micrometer-core/src/test/java/io/micrometer/core/aop/TimedAspectTest.java b/micrometer-core/src/test/java/io/micrometer/core/aop/TimedAspectTest.java index a44bda8927..592d890860 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/aop/TimedAspectTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/aop/TimedAspectTest.java @@ -23,6 +23,7 @@ import io.micrometer.core.instrument.Meter.Id; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.distribution.CountAtBucket; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.instrument.distribution.pause.PauseDetector; import io.micrometer.core.instrument.search.MeterNotFoundException; @@ -34,6 +35,7 @@ import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import javax.annotation.Nonnull; +import java.util.Arrays; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.Predicate; @@ -95,6 +97,29 @@ void timeMethodWithLongTaskTimer() { .size()).isEqualTo(1); } + @Test + void timeMethodWithSloTimer() { + MeterRegistry registry = new SimpleMeterRegistry(); + + AspectJProxyFactory pf = new AspectJProxyFactory(new TimedService()); + pf.addAspect(new TimedAspect(registry)); + + TimedService service = pf.getProxy(); + + service.sloCall(); + + assertThat(Arrays + .stream(registry.get("sloCall") + .tag("class", getClass().getName() + "$TimedService") + .tag("method", "sloCall") + .tag("extra", "tag") + .timer() + .takeSnapshot() + .histogramCounts()) + .mapToDouble(CountAtBucket::bucket) + .toArray()).isEqualTo(new double[] { Math.pow(10, 9) * 0.1, Math.pow(10, 9) * 0.5 }); + } + @Test void timeMethodFailure() { MeterRegistry failingRegistry = new FailingMeterRegistry(); @@ -621,6 +646,11 @@ void call() { void longCall() { } + @Timed(value = "sloCall", extraTags = { "extra", "tag" }, histogram = true, + serviceLevelObjectives = { 0.1, 0.5 }) + void sloCall() { + } + } static class AsyncTimedService { diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListenerTimedTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListenerTimedTest.java index b9c3207c5a..fa55243a97 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListenerTimedTest.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListenerTimedTest.java @@ -21,6 +21,7 @@ import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.binder.jersey.server.resources.TimedOnClassResource; import io.micrometer.core.instrument.binder.jersey.server.resources.TimedResource; +import io.micrometer.core.instrument.distribution.CountAtBucket; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; @@ -93,6 +94,20 @@ void resourcesWithAnnotationAreTimed() { assertThat(registry.get("multi2").tags(tagsFrom("/multi-timed", 200)).timer().count()).isEqualTo(1); } + @Test + void sloTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { + target("timed-slo").request().get(); + + CountAtBucket[] slos = registry.get("timedSlo") + .tags(tagsFrom("/timed-slo", 200)) + .timer() + .takeSnapshot() + .histogramCounts(); + assertThat(slos.length).isEqualTo(2); + assertThat(slos[0].bucket()).isEqualTo(Math.pow(10, 9) * 0.1); + assertThat(slos[1].bucket()).isEqualTo(Math.pow(10, 9) * 0.5); + } + @Test void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { final Future future = target("long-timed").request().async().get(); diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/resources/TimedResource.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/resources/TimedResource.java index 9777c22126..83e97968dc 100644 --- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/resources/TimedResource.java +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/jersey/server/resources/TimedResource.java @@ -54,6 +54,13 @@ public String timed() { return "timed"; } + @GET + @Path("timed-slo") + @Timed(value = "timedSlo", histogram = true, serviceLevelObjectives = { 0.1, 0.5 }) + public String timedSlo() { + return "timed"; + } + @GET @Path("multi-timed") @Timed("multi1")