diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java index 7d60cb969..1f65f0854 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinder.java @@ -33,6 +33,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; @@ -51,6 +53,8 @@ public class ExecutorServiceMetricsBinder implements BeanCreatedEventListener { private static final String THREAD_PER_TASK_EXECUTOR = "java.util.concurrent.ThreadPerTaskExecutor"; + private static final String NETTY_EVENT_LOOP_GROUP = "io.netty.channel.EventLoopGroup"; + private static final ConcurrentMap, Boolean> NETTY_EVENT_LOOP_GROUP_TYPES = new ConcurrentHashMap<>(); private final BeanProvider meterRegistryProvider; @@ -70,7 +74,7 @@ public ExecutorService onCreated(BeanCreatedEvent event) { unwrapped = ((InstrumentedExecutorService) unwrapped).getTarget(); } // Netty EventLoopGroups require separate instrumentation. - if (unwrapped.getClass().getName().startsWith("io.netty")) { + if (isNettyEventLoopGroup(unwrapped.getClass()) || unwrapped.getClass().getName().startsWith("io.netty")) { return unwrapped; } // ExecutorServiceMetrics does not provide metrics for virtual threads @@ -125,4 +129,24 @@ public Runnable instrument(Runnable command) { }; } } + + private static boolean isNettyEventLoopGroup(Class type) { + Boolean cached = NETTY_EVENT_LOOP_GROUP_TYPES.get(type); + if (cached != null) { + return cached; + } + boolean result = hasNettyEventLoopGroup(type); + NETTY_EVENT_LOOP_GROUP_TYPES.putIfAbsent(type, result); + return result; + } + + private static boolean hasNettyEventLoopGroup(Class type) { + for (Class interfaceType : type.getInterfaces()) { + if (NETTY_EVENT_LOOP_GROUP.equals(interfaceType.getName()) || isNettyEventLoopGroup(interfaceType)) { + return true; + } + } + Class superclass = type.getSuperclass(); + return superclass != null && isNettyEventLoopGroup(superclass); + } } diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEpollEventLoopGroupFactory.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEpollEventLoopGroupFactory.java index a1b97d827..996dd1df6 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEpollEventLoopGroupFactory.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEpollEventLoopGroupFactory.java @@ -22,7 +22,6 @@ import org.jspecify.annotations.Nullable; import io.micronaut.http.netty.channel.EpollAvailabilityCondition; import io.micronaut.http.netty.channel.EpollEventLoopGroupFactory; -import io.micronaut.http.netty.channel.EventLoopGroupFactory; import io.netty.channel.DefaultSelectStrategyFactory; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.Epoll; @@ -47,8 +46,8 @@ */ @Singleton @Internal -@Replaces(bean = EpollEventLoopGroupFactory.class, named = EventLoopGroupFactory.NATIVE) -@Named(EventLoopGroupFactory.NATIVE) +@Replaces(bean = EpollEventLoopGroupFactory.class, named = EpollEventLoopGroupFactory.NAME) +@Named(EpollEventLoopGroupFactory.NAME) @Requires(classes = Epoll.class, condition = EpollAvailabilityCondition.class) @RequiresMetrics @Requires(property = MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled", defaultValue = FALSE, notEquals = FALSE) diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEventLoopTaskQueueFactory.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEventLoopTaskQueueFactory.java index 22d0b8c90..599a071ad 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEventLoopTaskQueueFactory.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedEventLoopTaskQueueFactory.java @@ -20,16 +20,22 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; import io.micronaut.configuration.metrics.annotation.RequiresMetrics; +import io.micronaut.context.BeanContext; import io.micronaut.context.BeanProvider; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.http.server.netty.NettyHttpServer; +import io.micronaut.http.netty.channel.EventLoopGroupConfiguration; +import io.micronaut.http.netty.channel.TaskQueueInterceptor; +import io.micronaut.inject.qualifiers.Qualifiers; import io.netty.channel.EventLoopTaskQueueFactory; import io.netty.util.internal.PlatformDependent; import jakarta.inject.Named; import jakarta.inject.Singleton; import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.COUNT; @@ -58,65 +64,59 @@ @Requires(property = MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled", defaultValue = FALSE, notEquals = FALSE) @Requires(classes = EventLoopTaskQueueFactory.class) @Internal -final class InstrumentedEventLoopTaskQueueFactory implements EventLoopTaskQueueFactory { - - private static final AtomicInteger PARENT_COUNTER = new AtomicInteger(-1); - private static final AtomicInteger WORKER_COUNTER = new AtomicInteger(-1); +@SuppressWarnings("java:S1874") +final class InstrumentedEventLoopTaskQueueFactory implements EventLoopTaskQueueFactory, TaskQueueInterceptor { private final BeanProvider meterRegistryProvider; - private final Counter parentTaskCounter; - private final Counter workerTaskCounter; - private final Timer globalParentWaitTimeTimer; - private final Timer globalParentExecutionTimer; - private final Timer globalWorkerWaitTimeTimer; - private final Timer globalWorkerExecutionTimer; + private final BeanContext beanContext; + private final ConcurrentMap metrics = new ConcurrentHashMap<>(); /** * @param meterRegistryProvider the metric registry provider + * @param beanContext the bean context */ - public InstrumentedEventLoopTaskQueueFactory(BeanProvider meterRegistryProvider) { + public InstrumentedEventLoopTaskQueueFactory(BeanProvider meterRegistryProvider, + BeanContext beanContext) { this.meterRegistryProvider = meterRegistryProvider; - globalParentWaitTimeTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, WAIT_TIME)) - .description("Global wait time spent in the parent Queues.") - .tag(GROUP, PARENT) - .publishPercentileHistogram() - .register(meterRegistryProvider.get()); - globalParentExecutionTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, EXECUTION_TIME)) - .description("Global parent runnable execution time.") - .tag(GROUP, PARENT) - .publishPercentileHistogram() - .register(meterRegistryProvider.get()); - globalWorkerWaitTimeTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, WAIT_TIME)) - .description("Global wait time spent in the worker Queues.") - .tag(GROUP, WORKER) - .publishPercentileHistogram() - .register(meterRegistryProvider.get()); - globalWorkerExecutionTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, EXECUTION_TIME)) - .description("Global worker runnable execution time.") - .tag(GROUP, WORKER) - .publishPercentileHistogram() - .register(meterRegistryProvider.get()); - parentTaskCounter = Counter.builder(dot(NETTY, QUEUE, GLOBAL, ELEMENT, COUNT)) - .tag(GROUP, PARENT) - .register(meterRegistryProvider.get()); - workerTaskCounter = Counter.builder(dot(NETTY, QUEUE, GLOBAL, ELEMENT, COUNT)) - .tag(GROUP, WORKER) - .register(meterRegistryProvider.get()); + this.beanContext = beanContext; + metrics.put(PARENT, EventLoopGroupMetrics.create(meterRegistryProvider.get(), PARENT)); + metrics.put(WORKER, EventLoopGroupMetrics.create(meterRegistryProvider.get(), WORKER)); } @Override public Queue newTaskQueue(int maxCapacity) { final String kind = findOrigin(); - final boolean parent = PARENT.equals(kind); - return new MonitoredQueue(parent ? PARENT_COUNTER.incrementAndGet() : WORKER_COUNTER.incrementAndGet(), - meterRegistryProvider.get(), - Tag.of(GROUP, kind), - parent ? parentTaskCounter : workerTaskCounter, - parent ? globalParentWaitTimeTimer : globalWorkerWaitTimeTimer, - parent ? globalParentExecutionTimer : globalWorkerExecutionTimer, + return newMonitoredQueue(kind, maxCapacity == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue() : PlatformDependent.newMpscQueue(maxCapacity)); } + @Override + public Queue wrapTaskQueue(String groupName, Queue original) { + return newMonitoredQueue(groupName, original); + } + + private Queue newMonitoredQueue(String groupName, Queue queue) { + String name = normalizeGroupName(groupName); + EventLoopGroupMetrics groupMetrics = metrics.computeIfAbsent(name, group -> EventLoopGroupMetrics.create(meterRegistryProvider.get(), group)); + return new MonitoredQueue(groupMetrics.queueCounter.incrementAndGet(), + meterRegistryProvider.get(), + Tag.of(GROUP, name), + groupMetrics.taskCounter, + groupMetrics.globalWaitTimeTimer, + groupMetrics.globalExecutionTimer, + queue); + } + + private String normalizeGroupName(String groupName) { + if (groupName == null || groupName.isEmpty()) { + return WORKER; + } + if (PARENT.equals(groupName) || WORKER.equals(groupName) || EventLoopGroupConfiguration.DEFAULT.equals(groupName)) { + return groupName; + } + return beanContext.findBean(EventLoopGroupConfiguration.class, Qualifiers.byName(groupName)).isPresent() ? groupName : WORKER; + } + private String findOrigin() { for (StackTraceElement elt: Thread.currentThread().getStackTrace()) { if (NettyHttpServer.class.getName().equals(elt.getClassName()) && "createWorkerEventLoopGroup".equals(elt.getMethodName())) { @@ -129,4 +129,26 @@ private String findOrigin() { return WORKER; } + private record EventLoopGroupMetrics(AtomicInteger queueCounter, + Counter taskCounter, + Timer globalWaitTimeTimer, + Timer globalExecutionTimer) { + static EventLoopGroupMetrics create(MeterRegistry meterRegistry, String group) { + Timer globalWaitTimeTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, WAIT_TIME)) + .description("Global wait time spent in the event loop group queues.") + .tag(GROUP, group) + .publishPercentileHistogram() + .register(meterRegistry); + Timer globalExecutionTimer = Timer.builder(dot(NETTY, QUEUE, GLOBAL, EXECUTION_TIME)) + .description("Global runnable execution time for the event loop group.") + .tag(GROUP, group) + .publishPercentileHistogram() + .register(meterRegistry); + Counter taskCounter = Counter.builder(dot(NETTY, QUEUE, GLOBAL, ELEMENT, COUNT)) + .tag(GROUP, group) + .register(meterRegistry); + return new EventLoopGroupMetrics(new AtomicInteger(-1), taskCounter, globalWaitTimeTimer, globalExecutionTimer); + } + } + } diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedKQueueEventLoopGroupFactory.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedKQueueEventLoopGroupFactory.java index 3b5950773..e6d6bf3c3 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedKQueueEventLoopGroupFactory.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedKQueueEventLoopGroupFactory.java @@ -20,7 +20,6 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import org.jspecify.annotations.Nullable; -import io.micronaut.http.netty.channel.EventLoopGroupFactory; import io.micronaut.http.netty.channel.KQueueAvailabilityCondition; import io.micronaut.http.netty.channel.KQueueEventLoopGroupFactory; import io.netty.channel.DefaultSelectStrategyFactory; @@ -47,8 +46,8 @@ */ @Singleton @Internal -@Replaces(bean = KQueueEventLoopGroupFactory.class, named = EventLoopGroupFactory.NATIVE) -@Named(EventLoopGroupFactory.NATIVE) +@Replaces(bean = KQueueEventLoopGroupFactory.class, named = KQueueEventLoopGroupFactory.NAME) +@Named(KQueueEventLoopGroupFactory.NAME) @Requires(classes = KQueue.class, condition = KQueueAvailabilityCondition.class) @RequiresMetrics @Requires(property = MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled", defaultValue = FALSE, notEquals = FALSE) diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNativeEventLoopGroupFactoryAliases.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNativeEventLoopGroupFactoryAliases.java new file mode 100644 index 000000000..748bcc2d8 --- /dev/null +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNativeEventLoopGroupFactoryAliases.java @@ -0,0 +1,47 @@ +/* + * Copyright 2017-2026 original authors + * + * 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.micronaut.configuration.metrics.binder.netty; + +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.netty.channel.EventLoopGroupFactory; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +/** + * Backwards-compatible native event loop group factory aliases. + * + * @since 6.0.0 + */ +@Factory +@Internal +final class InstrumentedNativeEventLoopGroupFactoryAliases { + + @Singleton + @Named(EventLoopGroupFactory.NATIVE) + @Requires(beans = InstrumentedEpollEventLoopGroupFactory.class) + EventLoopGroupFactory epollNativeEventLoopGroupFactory(InstrumentedEpollEventLoopGroupFactory factory) { + return factory; + } + + @Singleton + @Named(EventLoopGroupFactory.NATIVE) + @Requires(beans = InstrumentedKQueueEventLoopGroupFactory.class) + EventLoopGroupFactory kQueueNativeEventLoopGroupFactory(InstrumentedKQueueEventLoopGroupFactory factory) { + return factory; + } +} diff --git a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNioEventLoopGroupFactory.java b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNioEventLoopGroupFactory.java index d3a126a9b..a62f10596 100644 --- a/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNioEventLoopGroupFactory.java +++ b/micrometer-core/src/main/java/io/micronaut/configuration/metrics/binder/netty/InstrumentedNioEventLoopGroupFactory.java @@ -27,6 +27,7 @@ import io.netty.util.concurrent.DefaultEventExecutorChooserFactory; import io.netty.util.concurrent.RejectedExecutionHandlers; import io.netty.util.concurrent.ThreadPerTaskExecutor; +import jakarta.inject.Named; import jakarta.inject.Singleton; import java.nio.channels.spi.SelectorProvider; @@ -43,6 +44,7 @@ * @since 2.0 */ @Singleton +@Named(NioEventLoopGroupFactory.NAME) @Internal @Replaces(bean = NioEventLoopGroupFactory.class) @RequiresMetrics diff --git a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinderSpec.groovy b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinderSpec.groovy index ecd856562..555978d69 100644 --- a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinderSpec.groovy +++ b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/executor/ExecutorServiceMetricsBinderSpec.groovy @@ -9,6 +9,7 @@ import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Requires import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.scheduling.TaskExecutors +import io.micronaut.scheduling.instrument.InstrumentedExecutorService import io.netty.channel.DefaultEventLoop import io.netty.channel.EventLoopGroup import jakarta.inject.Named @@ -79,6 +80,19 @@ class ExecutorServiceMetricsBinderSpec extends Specification { context.close() } + void "test event loop group outside netty package not instrumented"() { + when: + ApplicationContext context = ApplicationContext.run() + ExecutorService executorService = context.getBean(ExecutorService, Qualifiers.byName("delegating")) + + then: + executorService instanceof TestEventLoopGroup + !(executorService instanceof InstrumentedExecutorService) + + cleanup: + context.close() + } + @Issue("https://github.com/micronaut-projects/micronaut-micrometer/issues/679") void "test the virtual task executor is unbound with no warnings logged"() { when: @@ -128,6 +142,15 @@ class ExecutorServiceMetricsBinderSpec extends Specification { EventLoopGroup eventLoopGroup() { return new DefaultEventLoop() } + + @Singleton + @Named("delegating") + TestEventLoopGroup delegatingEventLoopGroup() { + return new TestEventLoopGroup() + } + } + + static class TestEventLoopGroup extends DefaultEventLoop { } class SimpleStreamsListener implements IStandardStreamsListener { diff --git a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/netty/MicronautNettyQueuesMetricsBinderSpec.groovy b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/netty/MicronautNettyQueuesMetricsBinderSpec.groovy index ee3f5fb06..8b92e08ff 100644 --- a/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/netty/MicronautNettyQueuesMetricsBinderSpec.groovy +++ b/micrometer-core/src/test/groovy/io/micronaut/configuration/metrics/binder/netty/MicronautNettyQueuesMetricsBinderSpec.groovy @@ -9,13 +9,20 @@ import io.micronaut.context.ApplicationContext import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.client.annotation.Client +import io.micronaut.http.netty.channel.EpollEventLoopGroupFactory import io.micronaut.http.netty.channel.EventLoopGroupFactory +import io.micronaut.http.netty.channel.KQueueEventLoopGroupFactory import io.micronaut.http.netty.channel.NettyChannelType +import io.micronaut.http.netty.channel.NioEventLoopGroupFactory +import io.micronaut.inject.qualifiers.Qualifiers import io.micronaut.runtime.server.EmbeddedServer +import io.netty.channel.EventLoopGroup import spock.lang.Unroll import spock.lang.Ignore import spock.lang.Specification +import java.util.concurrent.ConcurrentLinkedQueue + import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.COUNT import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.ELEMENT import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.EXECUTION_TIME @@ -24,6 +31,7 @@ import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.GROUP import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.NETTY import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.PARENT import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.QUEUE +import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.SIZE import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.WAIT_TIME import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.WORKER import static io.micronaut.configuration.metrics.binder.netty.NettyMetrics.dot @@ -59,6 +67,45 @@ class MicronautNettyQueuesMetricsBinderSpec extends Specification { MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled" | false | false } + @Unroll + void "test instrumented event loop group factory replaces #factoryName factory by name"() { + when: + ApplicationContext context = ApplicationContext.run( + [(MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled"): true] + ) + + then: + context.findBean(instrumentedClass).isPresent() == context.findBean(EventLoopGroupFactory, Qualifiers.byName(factoryName)) + .map { instrumentedClass.isInstance(it) } + .orElse(false) + + cleanup: + context.close() + + where: + instrumentedClass | factoryName + InstrumentedNioEventLoopGroupFactory | NioEventLoopGroupFactory.NAME + InstrumentedEpollEventLoopGroupFactory | EpollEventLoopGroupFactory.NAME + InstrumentedKQueueEventLoopGroupFactory | KQueueEventLoopGroupFactory.NAME + } + + void "test instrumented native event loop group factory is available by legacy name"() { + when: + ApplicationContext context = ApplicationContext.run( + [(MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled"): true] + ) + Optional nativeFactory = context.findBean(EventLoopGroupFactory, Qualifiers.byName(EventLoopGroupFactory.NATIVE)) + List availableNativeFactories = [InstrumentedEpollEventLoopGroupFactory, InstrumentedKQueueEventLoopGroupFactory] + .findAll { context.findBean(it).isPresent() } + + then: + nativeFactory.isPresent() == !availableNativeFactories.isEmpty() + !nativeFactory.isPresent() || availableNativeFactories.any { it.isInstance(nativeFactory.get()) } + + cleanup: + context.close() + } + @Unroll("test getting #channelType channel class from #eventLoopGroupFactory.getClass().getSimpleName()") void "test getting channel class from instrumented event loop group factory"( NettyChannelType channelType, @@ -97,6 +144,56 @@ class MicronautNettyQueuesMetricsBinderSpec extends Specification { new InstrumentedKQueueEventLoopGroupFactory(null) | true } + void "test configured client event loop queue metrics are present"() { + given: + ApplicationContext context = ApplicationContext.run( + [MICRONAUT_METRICS_ENABLED : true, + (MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled"): true, + "micronaut.netty.event-loops.client.num-threads" : 1, + "micronaut.http.client.event-loop-group" : "client"] + ) + + when: + EventLoopGroup eventLoopGroup = context.getBean(EventLoopGroup, Qualifiers.byName("client")) + MeterRegistry registry = context.getBean(MeterRegistry) + + then: + eventLoopGroup + registry.get(dot(NETTY, QUEUE, SIZE)) + .tags(Tags.of(GROUP, "client")) + .gauges() + .size() > 0 + + cleanup: + context.close() + } + + void "test unexpected event loop queue group names use worker metrics"() { + given: + ApplicationContext context = ApplicationContext.run( + [MICRONAUT_METRICS_ENABLED : true, + (MICRONAUT_METRICS_BINDERS + ".netty.queues.enabled"): true] + ) + + when: + InstrumentedEventLoopTaskQueueFactory factory = context.getBean(InstrumentedEventLoopTaskQueueFactory) + MeterRegistry registry = context.getBean(MeterRegistry) + factory.wrapTaskQueue("unexpected", new ConcurrentLinkedQueue()) + + then: + registry.find(dot(NETTY, QUEUE, SIZE)) + .tags(Tags.of(GROUP, "unexpected")) + .gauges() + .isEmpty() + registry.get(dot(NETTY, QUEUE, SIZE)) + .tags(Tags.of(GROUP, WORKER)) + .gauges() + .size() == 1 + + cleanup: + context.close() + } + @Ignore void "test queue metrics are present"() { when: diff --git a/src/main/docs/guide/metricsConcepts.adoc b/src/main/docs/guide/metricsConcepts.adoc index 6d63a86c8..3fa9e3fe6 100644 --- a/src/main/docs/guide/metricsConcepts.adoc +++ b/src/main/docs/guide/metricsConcepts.adoc @@ -243,11 +243,11 @@ If you use `io.micronaut.sql:micronaut-jdbc-tomcat` or `io.micronaut.sql:microna | jdbc.connections.min |======= -===== Netty Server Metrics +===== Netty Metrics -Currently, the following binders are provided to instrument Netty server: +Currently, the following binders are provided to instrument Netty: -* *EventLoopGroupFactoryBinder*: Instrument and expose event loop group queues metrics; use `micronaut.metrics.binders.netty.queues.enabled` to toggle. Default is *false*. The queues' size and tasks wait and execution time are exposed. +* *EventLoopGroupFactoryBinder*: Instrument and expose event loop group queues metrics; use `micronaut.metrics.binders.netty.queues.enabled` to toggle. Default is *false*. The queues' size and tasks wait and execution time are exposed. Metrics are tagged with the event loop group name for configured Netty server and client event loops. * *ByteBufAllocatorMetricsBinder*: Expose `ByteBuf` default allocators (https://netty.io/4.1/api/io/netty/buffer/ByteBufAllocatorMetric.html[UnpooledByteBufAllocator], https://netty.io/4.1/api/io/netty/buffer/PooledByteBufAllocatorMetric.html[PooledByteBufAllocator]) metrics; use `micronaut.metrics.binders.netty.bytebuf-allocators.enabled` to toggle. Default is *false*. You can customize what metrics are exposed using `micronaut.metrics.binders.netty.bytebuf-allocators.metrics`. By default, all available metrics are exposed. These flags are supported: ** `POOLED_ALLOCATOR`: expose `PooledByteBufAllocator` metrics,