From e4f6b4f6a7b75577e7a613ebade23b4e82310537 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 22 Sep 2023 19:09:57 -0700 Subject: [PATCH] Add shortcuts to assign dynamic tags to Meters Closes gh-535 See gh-4092 Co-authored-by: qweek --- .../micrometer/core/instrument/Counter.java | 18 +++ .../core/instrument/DistributionSummary.java | 18 +++ .../core/instrument/LongTaskTimer.java | 24 ++- .../io/micrometer/core/instrument/Timer.java | 23 ++- .../core/instrument/DynamicTagsTests.java | 144 ++++++++++++++++++ 5 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 micrometer-core/src/test/java/io/micrometer/core/instrument/DynamicTagsTests.java diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/Counter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Counter.java index 4edbdd7899..5c6b9d4f40 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Counter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Counter.java @@ -18,12 +18,14 @@ import io.micrometer.common.lang.Nullable; import java.util.Collections; +import java.util.function.Function; /** * Counters monitor monotonically increasing values. Counters may never be reset to a * lesser value. If you need to track a value that goes up and down, use a {@link Gauge}. * * @author Jon Schneider + * @author Jonatan Ivanov */ public interface Counter extends Meter { @@ -119,6 +121,18 @@ public Builder baseUnit(@Nullable String unit) { return this; } + /** + * Convenience method to create new meters from the builder that only differ in + * tags. This method can be used for dynamic tagging by creating the builder once + * and applying the dynamically changing tags using the returned {@link Function}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link Function} that returns a meter based on the provided tags. + * @since 1.12.0 + */ + public Function with(MeterRegistry registry) { + return extraTags -> register(registry, tags.and(extraTags)); + } + /** * Add the counter to a single registry, or return an existing counter in that * registry. The returned counter will be unique for each registry, but each @@ -128,6 +142,10 @@ public Builder baseUnit(@Nullable String unit) { * @return A new or existing counter. */ public Counter register(MeterRegistry registry) { + return register(registry, tags); + } + + private Counter register(MeterRegistry registry, Tags tags) { return registry.counter(new Meter.Id(name, tags, baseUnit, description, Type.COUNTER)); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/DistributionSummary.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/DistributionSummary.java index 3d497337ab..9c01789988 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/DistributionSummary.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/DistributionSummary.java @@ -24,12 +24,14 @@ import java.time.Duration; import java.util.Arrays; import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** * Track the sample distribution of events. An example would be the response sizes for * requests hitting an http server. * * @author Jon Schneider + * @author Jonatan Ivanov */ public interface DistributionSummary extends Meter, HistogramSupport { @@ -385,6 +387,18 @@ public Builder scale(double scale) { return this; } + /** + * Convenience method to create new meters from the builder that only differ in + * tags. This method can be used for dynamic tagging by creating the builder once + * and applying the dynamically changing tags using the returned {@link Function}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link Function} that returns a meter based on the provided tags. + * @since 1.12.0 + */ + public Function with(MeterRegistry registry) { + return extraTags -> register(registry, tags.and(extraTags)); + } + /** * Add the distribution summary to a single registry, or return an existing * distribution summary in that registry. The returned distribution summary will @@ -395,6 +409,10 @@ public Builder scale(double scale) { * @return A new or existing distribution summary. */ public DistributionSummary register(MeterRegistry registry) { + return register(registry, tags); + } + + private DistributionSummary register(MeterRegistry registry, Tags tags) { return registry.summary(new Meter.Id(name, tags, baseUnit, description, Type.DISTRIBUTION_SUMMARY), distributionConfigBuilder.build(), scale); } diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/LongTaskTimer.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/LongTaskTimer.java index dc1dee2b94..b99b6f6870 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/LongTaskTimer.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/LongTaskTimer.java @@ -24,18 +24,14 @@ import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import java.util.function.BooleanSupplier; -import java.util.function.Consumer; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; +import java.util.function.*; /** * A long task timer is used to track the total duration of all in-flight long-running * tasks and the number of such tasks. * * @author Jon Schneider + * @author Jonatan Ivanov */ public interface LongTaskTimer extends Meter, HistogramSupport { @@ -477,6 +473,18 @@ public Builder publishPercentileHistogram(@Nullable Boolean enabled) { return this; } + /** + * Convenience method to create new meters from the builder that only differ in + * tags. This method can be used for dynamic tagging by creating the builder once + * and applying the dynamically changing tags using the returned {@link Function}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link Function} that returns a meter based on the provided tags. + * @since 1.12.0 + */ + public Function with(MeterRegistry registry) { + return extraTags -> register(registry, tags.and(extraTags)); + } + /** * Add the long task timer to a single registry, or return an existing long task * timer in that registry. The returned long task timer will be unique for each @@ -487,6 +495,10 @@ public Builder publishPercentileHistogram(@Nullable Boolean enabled) { * @return A new or existing long task timer. */ public LongTaskTimer register(MeterRegistry registry) { + return register(registry, tags); + } + + private LongTaskTimer register(MeterRegistry registry, Tags tags) { return registry.more() .longTaskTimer(new Meter.Id(name, tags, null, description, Type.LONG_TASK_TIMER), distributionConfigBuilder.build()); 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 8e6c90caa7..09a3a86d2e 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 @@ -27,11 +27,7 @@ import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import java.util.function.BooleanSupplier; -import java.util.function.DoubleSupplier; -import java.util.function.IntSupplier; -import java.util.function.LongSupplier; -import java.util.function.Supplier; +import java.util.function.*; /** * Timer intended to track of a large number of short running events. Example would be @@ -40,6 +36,7 @@ * * @author Jon Schneider * @author Oleksii Bondar + * @author Jonatan Ivanov */ public interface Timer extends Meter, HistogramSupport { @@ -430,6 +427,18 @@ public Builder description(String description) { return super.description(description); } + /** + * Convenience method to create new meters from the builder that only differ in + * tags. This method can be used for dynamic tagging by creating the builder once + * and applying the dynamically changing tags using the returned {@link Function}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link Function} that returns a meter based on the provided tags. + * @since 1.12.0 + */ + public Function with(MeterRegistry registry) { + return extraTags -> register(registry, tags.and(extraTags)); + } + /** * Add the timer to a single registry, or return an existing timer in that * registry. The returned timer will be unique for each registry, but each @@ -439,6 +448,10 @@ public Builder description(String description) { * @return A new or existing timer. */ public Timer register(MeterRegistry registry) { + return register(registry, tags); + } + + private Timer register(MeterRegistry registry, Tags tags) { // the base unit for a timer will be determined by the monitoring system // implementation return registry.timer(new Meter.Id(name, tags, null, description, Type.TIMER), diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/DynamicTagsTests.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/DynamicTagsTests.java new file mode 100644 index 0000000000..20f0a7bdd6 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/DynamicTagsTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2023 VMware, Inc. + * + * 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.micrometer.core.instrument; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for convenience methods for dynamic tagging. + * + * @author Jonatan Ivanov + */ +class DynamicTagsTests { + + private MeterRegistry registry; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + } + + @Test + void shouldCreateCountersDynamically() { + Function counterFactory = Counter.builder("test.counter").tag("static", "abc").with(registry); + + counterFactory.apply(Tags.of("dynamic", "1")).increment(); + counterFactory.apply(Tags.of("dynamic", "2")).increment(); + counterFactory.apply(Tags.of("dynamic", "1")).increment(); + + assertThat(registry.getMeters()).hasSize(2); + assertThat(registry.find("test.counter").tags("static", "abc", "dynamic", "1").counters()).hasSize(1); + assertThat(registry.find("test.counter").tags("static", "abc", "dynamic", "2").counters()).hasSize(1); + } + + @Test + void shouldOverrideStaticTagsWhenCreatesCountersDynamically() { + Function counterFactory = Counter.builder("test.counter").tag("static", "abc").with(registry); + + counterFactory.apply(Tags.of("static", "xyz", "dynamic", "1")).increment(); + + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.find("test.counter").tags("static", "xyz", "dynamic", "1").counters()).hasSize(1); + } + + @Test + void shouldCreateTimersDynamically() { + Function timerFactory = Timer.builder("test.timer").tag("static", "abc").with(registry); + + timerFactory.apply(Tags.of("dynamic", "1")).record(Duration.ofMillis(100)); + timerFactory.apply(Tags.of("dynamic", "2")).record(Duration.ofMillis(200)); + timerFactory.apply(Tags.of("dynamic", "1")).record(Duration.ofMillis(100)); + + assertThat(registry.getMeters()).hasSize(2); + assertThat(registry.find("test.timer").tags("static", "abc", "dynamic", "1").timers()).hasSize(1); + assertThat(registry.find("test.timer").tags("static", "abc", "dynamic", "2").timers()).hasSize(1); + } + + @Test + void shouldOverrideStaticTagsWhenCreatesTimersDynamically() { + Function timerFactory = Timer.builder("test.timer").tag("static", "abc").with(registry); + + timerFactory.apply(Tags.of("static", "xyz", "dynamic", "1")).record(Duration.ofMillis(100)); + + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.find("test.timer").tags("static", "xyz", "dynamic", "1").timers()).hasSize(1); + } + + @Test + void shouldCreateLongTaskTimersDynamically() { + Function timerFactory = LongTaskTimer.builder("test.active.timer") + .tag("static", "abc") + .with(registry); + + timerFactory.apply(Tags.of("dynamic", "1")).start().stop(); + timerFactory.apply(Tags.of("dynamic", "2")).start().stop(); + timerFactory.apply(Tags.of("dynamic", "1")).start().stop(); + + assertThat(registry.getMeters()).hasSize(2); + assertThat(registry.find("test.active.timer").tags("static", "abc", "dynamic", "1").longTaskTimers()) + .hasSize(1); + assertThat(registry.find("test.active.timer").tags("static", "abc", "dynamic", "2").longTaskTimers()) + .hasSize(1); + } + + @Test + void shouldOverrideStaticTagsWhenCreatesLongTaskTimersDynamically() { + Function timerFactory = LongTaskTimer.builder("test.active.timer") + .tag("static", "abc") + .with(registry); + + timerFactory.apply(Tags.of("static", "xyz", "dynamic", "1")).start().stop(); + + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.find("test.active.timer").tags("static", "xyz", "dynamic", "1").longTaskTimers()) + .hasSize(1); + } + + @Test + void shouldCreateDistributionSummariesDynamically() { + Function distributionFactory = DistributionSummary.builder("test.distribution") + .tag("static", "abc") + .with(registry); + + distributionFactory.apply(Tags.of("dynamic", "1")).record(1); + distributionFactory.apply(Tags.of("dynamic", "2")).record(2); + distributionFactory.apply(Tags.of("dynamic", "1")).record(1); + + assertThat(registry.getMeters()).hasSize(2); + assertThat(registry.find("test.distribution").tags("static", "abc", "dynamic", "1").summaries()).hasSize(1); + assertThat(registry.find("test.distribution").tags("static", "abc", "dynamic", "2").summaries()).hasSize(1); + } + + @Test + void shouldOverrideStaticTagsWhenCreatesDistributionSummariesDynamically() { + Function distributionFactory = DistributionSummary.builder("test.distribution") + .tag("static", "abc") + .with(registry); + + distributionFactory.apply(Tags.of("static", "xyz", "dynamic", "1")).record(1); + + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.find("test.distribution").tags("static", "xyz", "dynamic", "1").summaries()).hasSize(1); + } + +}