From f61a4faad32be9276a8d25db70ae284006abd789 Mon Sep 17 00:00:00 2001
From: Jonatan Ivanov <jonatan.ivanov@gmail.com>
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 <alnovoselov@mail.ru>
---
 .../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<Counter> 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<DistributionSummary> 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<LongTaskTimer> 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 <T> Meter type
+     * @since 1.12.0
+     */
+    interface MeterProvider<T extends Meter> {
+
+        /**
+         * 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<? extends Tag> 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<Timer> 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<Counter> 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<Counter> 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<Timer> 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<Timer> 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<LongTaskTimer> 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<LongTaskTimer> 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<DistributionSummary> 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<DistributionSummary> 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);
+    }
+
+}