From f61a4faad32be9276a8d25db70ae284006abd789 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Mon, 9 Oct 2023 15:09:42 -0700 Subject: [PATCH] Add shortcuts to assign dynamic tags to Meters (#4097) Closes gh-535 See gh-4092 Co-authored-by: qweek --- .../micrometer/core/instrument/Counter.java | 19 +++ .../core/instrument/DistributionSummary.java | 19 +++ .../core/instrument/LongTaskTimer.java | 26 ++- .../io/micrometer/core/instrument/Meter.java | 42 +++++ .../io/micrometer/core/instrument/Timer.java | 25 ++- .../core/instrument/DynamicTagsTests.java | 148 ++++++++++++++++++ 6 files changed, 268 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..79101f6b13 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 @@ -24,6 +24,7 @@ * 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 +120,20 @@ public Builder baseUnit(@Nullable String unit) { return this; } + /** + * Convenience method to create 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 MeterProvider}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link MeterProvider} that returns a meter based on the provided + * tags. + * @since 1.12.0 + */ + public MeterProvider withRegistry(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 +143,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..5c2f46e052 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 @@ -30,6 +30,7 @@ * requests hitting an http server. * * @author Jon Schneider + * @author Jonatan Ivanov */ public interface DistributionSummary extends Meter, HistogramSupport { @@ -385,6 +386,20 @@ public Builder scale(double scale) { return this; } + /** + * Convenience method to create 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 MeterProvider}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link MeterProvider} that returns a meter based on the provided + * tags. + * @since 1.12.0 + */ + public MeterProvider withRegistry(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 +410,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..e04a757cf6 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,20 @@ public Builder publishPercentileHistogram(@Nullable Boolean enabled) { return this; } + /** + * Convenience method to create 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 MeterProvider}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link MeterProvider} that returns a meter based on the provided + * tags. + * @since 1.12.0 + */ + public MeterProvider withRegistry(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 +497,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/Meter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/Meter.java index d1e5c853cf..f47a0577be 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/Meter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/Meter.java @@ -35,6 +35,7 @@ * A named and dimensioned producer of one or more measurements. * * @author Jon Schneider + * @author Jonatan Ivanov */ public interface Meter { @@ -480,6 +481,47 @@ public Meter register(MeterRegistry registry) { } + /** + * Convenience interface to create new meters from tags based on a common + * "template"/builder. See usage in Meter implementations, e.g.: {@code Timer}, + * {@code Counter} + * + * @param Meter type + * @since 1.12.0 + */ + interface MeterProvider { + + /** + * Registers (creates a new or gets an existing one if already exists) Meters + * using the provided tags. + * @param tags Tags to attach to the Meter about to be registered + * @return A new or existing Meter + */ + T withTags(Iterable tags); + + /** + * Registers (creates a new or gets an existing one if already exists) Meters + * using the provided tags. + * @param tags Tags to attach to the Meter about to be registered + * @return A new or existing Meter + */ + default T withTags(String... tags) { + return withTags(Tags.of(tags)); + } + + /** + * Registers (creates a new or gets an existing one if already exists) Meters + * using the provided tags. + * @param key the tag key to add + * @param value the tag value to add + * @return A new or existing Meter + */ + default T withTag(String key, String value) { + return withTags(Tags.of(key, value)); + } + + } + default void close() { } 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..f7e8e475b9 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,20 @@ public Builder description(String description) { return super.description(description); } + /** + * Convenience method to create 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 MeterProvider}. + * @param registry A registry to add the meter to, if it doesn't already exist. + * @return A {@link MeterProvider} that returns a meter based on the provided + * tags. + * @since 1.12.0 + */ + public MeterProvider withRegistry(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 +450,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..eaa6881802 --- /dev/null +++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/DynamicTagsTests.java @@ -0,0 +1,148 @@ +/* + * 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.Meter.MeterProvider; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +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() { + MeterProvider counterProvider = Counter.builder("test.counter") + .tag("static", "abc") + .withRegistry(registry); + + counterProvider.withTags(Tags.of("dynamic", "1")).increment(); + counterProvider.withTags("dynamic", "2").increment(); + counterProvider.withTag("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() { + MeterProvider counterProvider = Counter.builder("test.counter") + .tag("static", "abc") + .withRegistry(registry); + + counterProvider.withTags(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() { + MeterProvider timerProvider = Timer.builder("test.timer").tag("static", "abc").withRegistry(registry); + + timerProvider.withTags(Tags.of("dynamic", "1")).record(Duration.ofMillis(100)); + timerProvider.withTags("dynamic", "2").record(Duration.ofMillis(200)); + timerProvider.withTag("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() { + MeterProvider timerProvider = Timer.builder("test.timer").tag("static", "abc").withRegistry(registry); + + timerProvider.withTags(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() { + MeterProvider timeProvider = LongTaskTimer.builder("test.active.timer") + .tag("static", "abc") + .withRegistry(registry); + + timeProvider.withTags(Tags.of("dynamic", "1")).start().stop(); + timeProvider.withTags("dynamic", "2").start().stop(); + timeProvider.withTag("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() { + MeterProvider timeProvider = LongTaskTimer.builder("test.active.timer") + .tag("static", "abc") + .withRegistry(registry); + + timeProvider.withTags(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() { + MeterProvider distributionProvider = DistributionSummary.builder("test.distribution") + .tag("static", "abc") + .withRegistry(registry); + + distributionProvider.withTags(Tags.of("dynamic", "1")).record(1); + distributionProvider.withTags("dynamic", "2").record(2); + distributionProvider.withTag("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() { + MeterProvider distributionProvider = DistributionSummary.builder("test.distribution") + .tag("static", "abc") + .withRegistry(registry); + + distributionProvider.withTags(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); + } + +}