Repository: micrometer-metrics/micrometer (2025-08-07T19:13:51Z)

Issue:
Support custom tags in NettyEventExecutorMetrics
I would like to be able to set custom tags on `NettyEventExecutorMetrics` like is possible with most of the other `MeterBinder` implementations.

I set common tags on all the meters I register, but I can't modify the configuration of the `MeterRegistry` as it may be injected and using something like `MeterRegistry.config().commonTags(...)` would potentially modify other meters that should not, in fact, have those tags.



Discussion:
Thanks for the issue and the PR!

I think this is a valid feature request but just let you know, you can do this today using a `MeterFilter`, see the docs: https://docs.micrometer.io/micrometer/reference/concepts/meter-filters.html#_transforming_metrics
```java
registry.config().meterFilter(new MeterFilter() {
    @Override
    public Meter.Id map(Meter.Id id) {
        if(id.getName().startsWith("test")) {
            return id.withTag("extra", "value"); 
        } 

        return id;
    }
});
```
@jonatan-ivanov, thanks for the feedback! Currently I'm using a `MeterFilter` almost exactly like that as a workaround, but I'd rather have a 1-line solution that's consistent with the way that I'm configuring the other `MeterBinder`s than a 10-line solution that's unique.
It occurred to me that a potential issue users will face is if multiple instances of the binder are used in the same application with different custom tags, and the Prometheus registry is used, people will run into the issue of having the same metric with a different set of tags, which the Prometheus client does not allow. Documentation about this is [here](https://docs.micrometer.io/micrometer/reference/implementations/prometheus.html#_limitation_on_same_name_with_different_set_of_tag_keys) and we've run into this issue with ExecutorService metrics. I'm not sure about typical applications and these netty metrics. Perhaps it won't be a common issue, but it may be something to keep in mind if users will run into that situation.
That's definitely going to be a problem for me since I will have multiple Netty `EventLoopGroup`s to measure concurrently. 

Should I sent another PR to allow a custom name instead of the hard-coded value of `NettyMeters.EVENT_EXECUTOR_TASKS_PENDING.getName()`?
What set of tags do you want to attach that might be different?
Just to make sure we are on the same page, this is ok (setting a value to "none/unknown" if it is missing):
```
test {a="b", name="test"}
test {a="b", name="unknown"}
```
But leaving it completely out is an issue:
```
test {a="b", name="test"}
test {a="b"}
```
@jonatan-ivanov, thanks for the clarification. I misunderstood previously. I think everything's good on this front. 👍 



Patch:
diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetrics.java
index d3bf69b46a..f3377a3d44 100644
--- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetrics.java
+++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetrics.java
@@ -17,6 +17,8 @@
 
 import io.micrometer.core.instrument.Gauge;
 import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
 import io.micrometer.core.instrument.binder.MeterBinder;
 import io.netty.channel.EventLoop;
 import io.netty.util.concurrent.EventExecutor;
@@ -24,6 +26,24 @@
 
 /**
  * {@link MeterBinder} for Netty event executors.
+ * <p>
+ * An {@link io.netty.channel.EventLoopGroup} (all its executors) can be instrumented at
+ * startup like: <pre>
+ * MeterRegistry registry = //...
+ * EventLoopGroup group = //...
+ * new NettyEventExecutorMetrics(group).bindTo(registry);
+ * </pre> Alternatively, an {@link EventLoop} can be instrumented at runtime during
+ * channel initialization. In this case, developers should ensure that this instance has
+ * not been registered already as re-binding metrics at runtime is inefficient here. <pre>
+ * &#064;Override
+ * public void initChannel(SocketChannel channel) throws Exception {
+ *   // this concurrent check must be implemented by micrometer users
+ *   if (!isEventLoopInstrumented(channel.eventLoop())) {
+ *     new EventExecutorMetrics(channel.eventLoop()).bindTo(registry);
+ *   }
+ *   //...
+ * }
+ * </pre>
  *
  * @author Brian Clozel
  * @since 1.11.0
@@ -33,31 +53,26 @@ public class NettyEventExecutorMetrics implements MeterBinder {
 
     private final Iterable<EventExecutor> eventExecutors;
 
+    private final Iterable<Tag> tags;
+
     /**
      * Create a binder instance for the given event executors.
-     * <p>
-     * An {@link io.netty.channel.EventLoopGroup} (all its executors) can be instrumented
-     * at startup like: <pre>
-     * MeterRegistry registry = //...
-     * EventLoopGroup group = //...
-     * new NettyEventExecutorMetrics(group).bindTo(registry);
-     * </pre> Alternatively, an {@link EventLoop} can be instrumented at runtime during
-     * channel initialization. In this case, developers should ensure that this instance
-     * has not been registered already as re-binding metrics at runtime is inefficient
-     * here. <pre>
-     * &#064;Override
-     * public void initChannel(SocketChannel channel) throws Exception {
-     *   // this concurrent check must be implemented by micrometer users
-     *   if (!isEventLoopInstrumented(channel.eventLoop())) {
-     *     new EventExecutorMetrics(channel.eventLoop()).bindTo(registry);
-     *   }
-     *   //...
-     * }
-     * </pre>
      * @param eventExecutors the event executors to instrument
      */
     public NettyEventExecutorMetrics(Iterable<EventExecutor> eventExecutors) {
+        this(eventExecutors, Tags.empty());
+    }
+
+    /**
+     * Create a binder instance for the given event executors with tags applied to all
+     * Meters created by this binder.
+     * @param eventExecutors the event executors to instrument
+     * @param tags tags to apply to all Meters
+     * @since 1.16.0
+     */
+    public NettyEventExecutorMetrics(Iterable<EventExecutor> eventExecutors, Iterable<Tag> tags) {
         this.eventExecutors = eventExecutors;
+        this.tags = tags;
     }
 
     @Override
@@ -70,6 +85,7 @@ public void bindTo(MeterRegistry registry) {
                             singleThreadEventExecutor::pendingTasks)
                     .tag(NettyMeters.EventExecutorTasksPendingKeyNames.NAME.asString(),
                             singleThreadEventExecutor.threadProperties().name())
+                    .tags(tags)
                     .register(registry);
             }
         });


Test patch:
diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetricsTests.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetricsTests.java
index 1e0a2f3156..2710a495cb 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetricsTests.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/netty4/NettyEventExecutorMetricsTests.java
@@ -26,6 +26,7 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import static io.micrometer.core.instrument.binder.netty4.NettyMeters.EVENT_EXECUTOR_TASKS_PENDING;
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
@@ -50,7 +51,7 @@ void shouldHaveTasksPendingMetricForEachEventLoop() throws Exception {
         });
         assertThat(names).isNotEmpty();
         names.forEach(name -> {
-            assertThat(this.registry.get(NettyMeters.EVENT_EXECUTOR_TASKS_PENDING.getName())
+            assertThat(this.registry.get(EVENT_EXECUTOR_TASKS_PENDING.getName())
                 .tags(Tags.of("name", name))
                 .gauge()
                 .value()).isZero();
@@ -66,11 +67,28 @@ void shouldHaveTasksPendingMetricForSingleEventLoop() throws Exception {
         assertThat(eventLoop).isInstanceOf(SingleThreadEventExecutor.class);
         SingleThreadEventExecutor singleThreadEventExecutor = (SingleThreadEventExecutor) eventLoop;
         String eventLoopName = singleThreadEventExecutor.threadProperties().name();
-        assertThat(this.registry.get(NettyMeters.EVENT_EXECUTOR_TASKS_PENDING.getName())
+        assertThat(this.registry.get(EVENT_EXECUTOR_TASKS_PENDING.getName())
             .tags(Tags.of("name", eventLoopName))
             .gauge()
             .value()).isZero();
         eventExecutors.shutdownGracefully().get(5, TimeUnit.SECONDS);
     }
 
+    @Test
+    void shouldHaveCustomTags() throws Exception {
+        DefaultEventLoopGroup eventExecutors = new DefaultEventLoopGroup();
+        EventLoop eventLoop = eventExecutors.next();
+        Tags extraTags = Tags.of("testKey", "testValue");
+        new NettyEventExecutorMetrics(eventLoop, extraTags).bindTo(this.registry);
+        assertThat(eventLoop).isInstanceOf(SingleThreadEventExecutor.class);
+        SingleThreadEventExecutor singleThreadEventExecutor = (SingleThreadEventExecutor) eventLoop;
+        String eventLoopName = singleThreadEventExecutor.threadProperties().name();
+        assertThat(this.registry.get(EVENT_EXECUTOR_TASKS_PENDING.getName())
+            .tags(Tags.of("name", eventLoopName))
+            .tags(extraTags)
+            .gauge()
+            .value()).isZero();
+        eventExecutors.shutdownGracefully().get(5, TimeUnit.SECONDS);
+    }
+
 }