diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 195bc4a363..e9ad4648d1 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -5,9 +5,11 @@ + + diff --git a/implementations/micrometer-registry-health/build.gradle b/implementations/micrometer-registry-health/build.gradle index 7ad798c7c0..87666526bc 100644 --- a/implementations/micrometer-registry-health/build.gradle +++ b/implementations/micrometer-registry-health/build.gradle @@ -1,5 +1,6 @@ dependencies { api project(':micrometer-core') + api project(':micrometer-binders') implementation 'org.slf4j:slf4j-api' diff --git a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/JvmServiceLevelObjectives.java b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/JvmServiceLevelObjectives.java index 9b180ec9ca..e5a43f442b 100644 --- a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/JvmServiceLevelObjectives.java +++ b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/JvmServiceLevelObjectives.java @@ -15,9 +15,9 @@ */ package io.micrometer.health.objectives; -import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.binder.jvm.JvmGcMetrics; +import io.micrometer.binder.jvm.JvmHeapPressureMetrics; +import io.micrometer.binder.jvm.JvmMemoryMetrics; import io.micrometer.health.ServiceLevelObjective; import java.time.Duration; diff --git a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/OperatingSystemServiceLevelObjectives.java b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/OperatingSystemServiceLevelObjectives.java index 2a849ddf9d..15f92db524 100644 --- a/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/OperatingSystemServiceLevelObjectives.java +++ b/implementations/micrometer-registry-health/src/main/java/io/micrometer/health/objectives/OperatingSystemServiceLevelObjectives.java @@ -15,7 +15,7 @@ */ package io.micrometer.health.objectives; -import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; +import io.micrometer.binder.system.FileDescriptorMetrics; import io.micrometer.health.ServiceLevelObjective; /** diff --git a/implementations/micrometer-registry-health/src/test/java/io/micrometer/health/HealthMeterRegistryTest.java b/implementations/micrometer-registry-health/src/test/java/io/micrometer/health/HealthMeterRegistryTest.java index 9de4871ee8..6f8ad86d85 100644 --- a/implementations/micrometer-registry-health/src/test/java/io/micrometer/health/HealthMeterRegistryTest.java +++ b/implementations/micrometer-registry-health/src/test/java/io/micrometer/health/HealthMeterRegistryTest.java @@ -18,7 +18,7 @@ import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MockClock; import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.health.objectives.JvmServiceLevelObjectives; import org.junit.jupiter.api.Test; diff --git a/implementations/micrometer-registry-statsd/build.gradle b/implementations/micrometer-registry-statsd/build.gradle index 0fd0573087..3fafd8f40a 100644 --- a/implementations/micrometer-registry-statsd/build.gradle +++ b/implementations/micrometer-registry-statsd/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { api project(':micrometer-core') + api project(':micrometer-binders') implementation 'io.projectreactor:reactor-core' implementation('io.projectreactor.netty:reactor-netty-core') { diff --git a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/LogbackMetricsSuppressingFluxSink.java b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/LogbackMetricsSuppressingFluxSink.java index 335bcff32a..c8a8b32b81 100644 --- a/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/LogbackMetricsSuppressingFluxSink.java +++ b/implementations/micrometer-registry-statsd/src/main/java/io/micrometer/statsd/internal/LogbackMetricsSuppressingFluxSink.java @@ -17,7 +17,7 @@ import java.util.function.LongConsumer; -import io.micrometer.core.instrument.binder.logging.LogbackMetrics; +import io.micrometer.binder.logging.LogbackMetrics; import reactor.core.Disposable; import reactor.core.publisher.FluxSink; import reactor.util.context.Context; diff --git a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java index 64ef66bb21..b4b419444b 100644 --- a/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java +++ b/implementations/micrometer-registry-statsd/src/test/java/io/micrometer/statsd/StatsdMeterRegistryTest.java @@ -19,7 +19,7 @@ import ch.qos.logback.classic.Logger; import io.micrometer.core.Issue; import io.micrometer.core.instrument.*; -import io.micrometer.core.instrument.binder.logging.LogbackMetrics; +import io.micrometer.binder.logging.LogbackMetrics; import io.micrometer.core.instrument.config.NamingConvention; import io.micrometer.core.lang.Nullable; import org.junit.jupiter.api.AfterEach; diff --git a/micrometer-binders/build.gradle b/micrometer-binders/build.gradle new file mode 100644 index 0000000000..450393da47 --- /dev/null +++ b/micrometer-binders/build.gradle @@ -0,0 +1,149 @@ +plugins { + id 'idea' +} + +dependencies { + // TODO(anuraaga): HdrHistogram is exposed in the micrometer API but probably shouldn't be + api 'org.hdrhistogram:HdrHistogram' + implementation('org.latencyutils:LatencyUtils') { + exclude group: 'org.hdrhistogram', module: 'HdrHistogram' + } + + api project(':micrometer-core') + + // cache monitoring + optionalApi 'com.google.guava:guava' + optionalApi 'com.github.ben-manes.caffeine:caffeine' + optionalApi 'net.sf.ehcache:ehcache' + optionalApi 'javax.cache:cache-api' + optionalApi 'com.hazelcast:hazelcast' + optionalApi 'org.hibernate:hibernate-entitymanager' + + // server runtime monitoring + optionalApi 'org.eclipse.jetty:jetty-server' + optionalApi 'org.eclipse.jetty:jetty-client' + optionalApi 'org.apache.tomcat.embed:tomcat-embed-core' + optionalApi 'org.glassfish.jersey.core:jersey-server' + optionalApi 'io.grpc:grpc-api' + + // apache httpcomponents monitoring + optionalApi 'org.apache.httpcomponents:httpclient' + optionalApi 'org.apache.httpcomponents:httpasyncclient' + + // hystrix monitoring + // TODO: Mark deprecated in 1.9 in old code + // TODO: Make it noop in 1.9 in new code and in 2.0 + // TODO: Marked for removal in 2.1.0? + optionalApi 'com.netflix.hystrix:hystrix-core' + + // log monitoring + optionalApi 'ch.qos.logback:logback-classic' + optionalApi 'org.apache.logging.log4j:log4j-core' + + // @Timed AOP + optionalApi 'com.squareup.okhttp3:okhttp' + + optionalApi 'org.mongodb:mongodb-driver-sync' + + optionalApi 'org.jooq:jooq' + + optionalApi 'org.apache.kafka:kafka-clients' + optionalApi 'org.apache.kafka:kafka-streams' + + testImplementation 'io.projectreactor:reactor-test' + + // JUnit 5 + testImplementation 'org.junit.jupiter:junit-jupiter' + testImplementation 'com.tngtech.archunit:archunit-junit5' + + // Eclipse still needs this (as of 4.7.1a) + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + testImplementation 'org.mockito:mockito-inline' + + testImplementation 'org.hsqldb:hsqldb' + + // dependency injection tests + testImplementation 'javax.inject:javax.inject' + testImplementation 'org.springframework:spring-context' + testImplementation 'com.google.inject:guice' + + testImplementation 'com.h2database:h2' + + // Uncomment these if you are interested in testing injection with dagger in MeterRegistryInjectionTest +// testImplementation 'com.google.dagger:dagger' +// testAnnotationProcessor 'com.google.dagger:dagger-compiler' + + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.awaitility:awaitility' + + testImplementation 'org.ehcache:ehcache' + + testImplementation 'org.apache.kafka:kafka-clients' + + testImplementation 'org.apache.commons:commons-pool2:2.+' + + testImplementation 'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-inmemory' + testRuntimeOnly 'org.glassfish.jersey.inject:jersey-hk2' + + testImplementation 'ru.lanwen.wiremock:wiremock-junit5' + testImplementation 'com.github.tomakehurst:wiremock-jre8-standalone' + + // Log4j2 Async dependency + testImplementation 'com.lmax:disruptor:3.4.+' + + // Kafka binder IT dependencies + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:kafka' + + // Postgres Binder IT dependencies + testImplementation 'org.testcontainers:postgresql' + testImplementation 'org.postgresql:postgresql' + + testImplementation 'org.testcontainers:mongodb' +} + +task shenandoahTest(type: Test) { + // set heap size for the test JVM(s) + maxHeapSize = "1500m" + + useJUnitPlatform { + includeTags 'gc' + } + + jvmArgs '-XX:+UseShenandoahGC' +} + +task zgcTest(type: Test) { + // set heap size for the test JVM(s) + maxHeapSize = "1500m" + + useJUnitPlatform { + includeTags 'gc' + } + + jvmArgs '-XX:+UseZGC' +} + +task openj9BalancedTest(type: Test) { + // set heap size for the test JVM(s) + maxHeapSize = "1500m" + + useJUnitPlatform { + includeTags 'gc' + } + + jvmArgs '-Xgcpolicy:balanced' +} + +task openj9ConcurrentScavengeTest(type: Test) { + // set heap size for the test JVM(s) + maxHeapSize = "1500m" + + useJUnitPlatform { + includeTags 'gc' + } + + jvmArgs '-Xgc:concurrentScavenge' +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/CacheMeterBinder.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CacheMeterBinder.java new file mode 100644 index 0000000000..46c52d716d --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CacheMeterBinder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.lang.ref.WeakReference; + +import io.micrometer.core.instrument.FunctionCounter; +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.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +/** + * A common base class for cache metrics that ensures that all caches are instrumented + * with the same basic set of metrics while allowing for additional detail that is specific + * to an individual implementation. + *

+ * Having this common base set of metrics ensures that you can reason about basic cache performance + * in a dimensional slice that spans different cache implementations in your application. + * + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public abstract class CacheMeterBinder implements MeterBinder { + private final WeakReference cacheRef; + private final Iterable tags; + + public CacheMeterBinder(C cache, String cacheName, Iterable tags) { + this.tags = Tags.concat(tags, "cache", cacheName); + this.cacheRef = new WeakReference<>(cache); + } + + @Nullable + protected C getCache() { + return cacheRef.get(); + } + + @Override + public final void bindTo(MeterRegistry registry) { + C cache = getCache(); + if (size() != null) { + Gauge.builder("cache.size", cache, + c -> { + Long size = size(); + return size == null ? 0 : size; + }) + .tags(tags) + .description("The number of entries in this cache. This may be an approximation, depending on the type of cache.") + .register(registry); + } + + if (missCount() != null) { + FunctionCounter.builder("cache.gets", cache, + c -> { + Long misses = missCount(); + return misses == null ? 0 : misses; + }) + .tags(tags).tag("result", "miss") + .description("the number of times cache lookup methods have returned an uncached (newly loaded) value, or null") + .register(registry); + } + + FunctionCounter.builder("cache.gets", cache, c -> hitCount()) + .tags(tags).tag("result", "hit") + .description("The number of times cache lookup methods have returned a cached value.") + .register(registry); + + FunctionCounter.builder("cache.puts", cache, c -> putCount()) + .tags(tags) + .description("The number of entries added to the cache") + .register(registry); + + if (evictionCount() != null) { + FunctionCounter.builder("cache.evictions", cache, + c -> { + Long evictions = evictionCount(); + return evictions == null ? 0 : evictions; + }) + .tags(tags) + .description("cache evictions") + .register(registry); + } + + bindImplementationSpecificMetrics(registry); + } + + /** + * MOST cache implementations provide a means of retrieving the number of entries. Even if + * + * @return Total number of cache entries. This value may go up or down with puts, removes, and evictions. Returns + * {@code null} if the cache implementation does not provide a way to track cache size. + */ + @Nullable + protected abstract Long size(); + + /** + * @return Get requests that resulted in a "hit" against an existing cache entry. Monotonically increasing hit count. + */ + protected abstract long hitCount(); + + /** + * @return Get requests that resulted in a "miss", or didn't match an existing cache entry. Monotonically increasing count. + * Returns {@code null} if the cache implementation does not provide a way to track miss count, especially in distributed + * caches. + */ + @Nullable + protected abstract Long missCount(); + + /** + * @return Total number of entries that have been evicted from the cache. Monotonically increasing eviction count. + * Returns {@code null} if the cache implementation does not support eviction, or does not provide a way to track + * the eviction count. + */ + @Nullable + protected abstract Long evictionCount(); + + /** + * The put mechanism is unimportant - this count applies to entries added to the cache according to a pre-defined + * load function such as exists in Guava/Caffeine caches as well as manual puts. + * + * Note that Guava/Caffeine caches don't count manual puts. + * + * @return Total number of entries added to the cache. Monotonically increasing count. + */ + protected abstract long putCount(); + + /** + * Bind detailed metrics that are particular to the cache implementation, e.g. load duration for + * Caffeine caches, heap and disk size for EhCache caches. These metrics are above and beyond the + * basic set of metrics that is common to all caches. + * + * @param registry The registry to bind metrics to. + */ + protected abstract void bindImplementationSpecificMetrics(MeterRegistry registry); + + protected Iterable getTagsWithCacheName() { + return tags; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineCacheMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineCacheMetrics.java new file mode 100644 index 0000000000..39e121de95 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineCacheMetrics.java @@ -0,0 +1,210 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.ToLongFunction; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +/** + * Collect metrics from Caffeine's {@link Cache}. {@link CaffeineStatsCounter} is an + * alternative that can collect more detailed statistics. + *

+ * Note that {@code recordStats()} is required to gather non-zero statistics: + *

{@code
+ * Cache cache = Caffeine.newBuilder().recordStats().build();
+ * CaffeineCacheMetrics.monitor(registry, cache, "mycache", "region", "test");
+ * }
+ * + * @author Clint Checketts + * @see CaffeineStatsCounter + */ +@NonNullApi +@NonNullFields +public class CaffeineCacheMetrics> extends CacheMeterBinder { + + /** + * Creates a new {@link CaffeineCacheMetrics} instance. + * + * @param cache The cache to be instrumented. You must call {@link Caffeine#recordStats()} prior to building the cache + * for metrics to be recorded. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags tags to apply to all recorded metrics. + */ + public CaffeineCacheMetrics(C cache, String cacheName, Iterable tags) { + super(cache, cacheName, tags); + } + + /** + * Record metrics on a Caffeine cache. You must call {@link Caffeine#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + return monitor(registry, cache, cacheName, Tags.of(tags)); + } + + /** + * Record metrics on a Caffeine cache. You must call {@link Caffeine#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags Tags to apply to all recorded metrics. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + * @see CacheStats + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + new CaffeineCacheMetrics<>(cache, cacheName, tags).bindTo(registry); + return cache; + } + + /** + * Record metrics on a Caffeine cache. You must call {@link Caffeine#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + return monitor(registry, cache, cacheName, Tags.of(tags)); + } + + /** + * Record metrics on a Caffeine cache. You must call {@link Caffeine#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags Tags to apply to all recorded metrics. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + * @see CacheStats + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + monitor(registry, cache.synchronous(), cacheName, tags); + return cache; + } + + @Override + protected Long size() { + return getOrDefault(Cache::estimatedSize, null); + } + + @Override + protected long hitCount() { + return getOrDefault(c -> c.stats().hitCount(), 0L); + } + + @Override + protected Long missCount() { + return getOrDefault(c -> c.stats().missCount(), null); + } + + @Override + protected Long evictionCount() { + return getOrDefault(c -> c.stats().evictionCount(), null); + } + + @Override + protected long putCount() { + return getOrDefault(c -> c.stats().loadCount(), 0L); + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + C cache = getCache(); + FunctionCounter.builder("cache.eviction.weight", cache, c -> c.stats().evictionWeight()) + .tags(getTagsWithCacheName()) + .description("The sum of weights of evicted entries. This total does not include manual invalidations.") + .register(registry); + + if (cache instanceof LoadingCache) { + // dividing these gives you a measure of load latency + TimeGauge.builder("cache.load.duration", cache, TimeUnit.NANOSECONDS, c -> c.stats().totalLoadTime()) + .tags(getTagsWithCacheName()) + .description("The time the cache has spent loading new values") + .register(registry); + + FunctionCounter.builder("cache.load", cache, c -> c.stats().loadSuccessCount()) + .tags(getTagsWithCacheName()) + .tags("result", "success") + .description("The number of times cache lookup methods have successfully loaded a new value") + .register(registry); + + FunctionCounter.builder("cache.load", cache, c -> c.stats().loadFailureCount()) + .tags(getTagsWithCacheName()) + .tags("result", "failure") + .description("The number of times {@link Cache} lookup methods failed to load a new value, either " + + "because no value was found or an exception was thrown while loading") + .register(registry); + } + } + + @Nullable + private Long getOrDefault(Function function, @Nullable Long defaultValue) { + C cache = getCache(); + if (cache != null) { + return function.apply(cache); + } + + return defaultValue; + } + + private long getOrDefault(ToLongFunction function, long defaultValue) { + C cache = getCache(); + if (cache != null) { + return function.applyAsLong(cache); + } + + return defaultValue; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineStatsCounter.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineStatsCounter.java new file mode 100644 index 0000000000..bff2027c9c --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/CaffeineStatsCounter.java @@ -0,0 +1,173 @@ +/* + * Copyright 2021 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.binder.cache; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import com.github.benmanes.caffeine.cache.stats.StatsCounter; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +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.Timer; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Objects.requireNonNull; + +/** + * A {@link StatsCounter} instrumented with Micrometer. This will provide more detailed metrics than using + * {@link CaffeineCacheMetrics}. + *

+ * Note that this doesn't instrument the cache's size by default. Use {@link #registerSizeMetric(Cache)} to do so after + * the cache has been built. + *

+ * Use {@link com.github.benmanes.caffeine.cache.Caffeine#recordStats} to supply this class to the cache builder: + *

{@code
+ * MeterRegistry registry = ...;
+ * Cache graphs = Caffeine.newBuilder()
+ *     .maximumSize(10_000)
+ *     .recordStats(() -> new CaffeineStatsCounter(registry, "graphs"))
+ *     .build();
+ * }
+ * + * @author Ben Manes + * @author John Karp + * @author Johnny Lim + * @see CaffeineCacheMetrics + * @since 1.7.0 + */ +@NonNullApi +@NonNullFields +public final class CaffeineStatsCounter implements StatsCounter { + + private final MeterRegistry registry; + private final Tags tags; + + private final Counter hitCount; + private final Counter missCount; + private final Timer loadSuccesses; + private final Timer loadFailures; + private final EnumMap evictionMetrics; + + /** + * Constructs an instance for use by a single cache. + * + * @param registry the registry of metric instances + * @param cacheName will be used to tag metrics with "cache". + */ + public CaffeineStatsCounter(MeterRegistry registry, String cacheName) { + this(registry, cacheName, Tags.empty()); + } + + /** + * Constructs an instance for use by a single cache. + * + * @param registry the registry of metric instances + * @param cacheName will be used to tag metrics with "cache". + * @param extraTags tags to apply to all recorded metrics. + */ + public CaffeineStatsCounter(MeterRegistry registry, String cacheName, Iterable extraTags) { + requireNonNull(registry); + requireNonNull(cacheName); + requireNonNull(extraTags); + this.registry = registry; + this.tags = Tags.concat(extraTags, "cache", cacheName); + + hitCount = Counter.builder("cache.gets").tag("result", "hit").tags(tags) + .description("The number of times cache lookup methods have returned a cached value.") + .register(registry); + missCount = Counter.builder("cache.gets").tag("result", "miss").tags(tags) + .description("The number of times cache lookup methods have returned an uncached (newly loaded) value.") + .register(registry); + loadSuccesses = Timer.builder("cache.loads").tag("result", "success").tags(tags) + .description("Successful cache loads.").register(registry); + loadFailures = Timer.builder("cache.loads").tag("result", "failure").tags(tags) + .description("Failed cache loads.").register(registry); + + evictionMetrics = new EnumMap<>(RemovalCause.class); + Arrays.stream(RemovalCause.values()).forEach(cause -> evictionMetrics.put( + cause, + DistributionSummary.builder("cache.evictions").tag("cause", cause.name()).tags(tags) + .description("Entries evicted from cache.").register(registry))); + } + + /** + * Register a gauge for the size of the given cache. + * + * @param cache cache to register a gauge for its size + */ + public void registerSizeMetric(Cache cache) { + Gauge.builder("cache.size", cache, Cache::estimatedSize).tags(tags) + .description("The approximate number of entries in this cache.") + .register(registry); + } + + @Override + public void recordHits(int count) { + hitCount.increment(count); + } + + @Override + public void recordMisses(int count) { + missCount.increment(count); + } + + @Override + public void recordLoadSuccess(long loadTime) { + loadSuccesses.record(loadTime, TimeUnit.NANOSECONDS); + } + + @Override + public void recordLoadFailure(long loadTime) { + loadFailures.record(loadTime, TimeUnit.NANOSECONDS); + } + + @SuppressWarnings("deprecation") + public void recordEviction() { + } + + @Override + public void recordEviction(int weight, RemovalCause cause) { + evictionMetrics.get(cause).record(weight); + } + + @Override + public CacheStats snapshot() { + return CacheStats.of( + (long) hitCount.count(), + (long) missCount.count(), + loadSuccesses.count(), + loadFailures.count(), + (long) loadSuccesses.totalTime(TimeUnit.NANOSECONDS) + + (long) loadFailures.totalTime(TimeUnit.NANOSECONDS), + evictionMetrics.values().stream().mapToLong(DistributionSummary::count).sum(), + (long) evictionMetrics.values().stream().mapToDouble(DistributionSummary::totalAmount).sum() + ); + } + + @Override + public String toString() { + return snapshot().toString(); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/EhCache2Metrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/EhCache2Metrics.java new file mode 100644 index 0000000000..0f39b30ff4 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/EhCache2Metrics.java @@ -0,0 +1,233 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.function.Function; +import java.util.function.ToLongFunction; + +import io.micrometer.core.instrument.FunctionCounter; +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.BaseUnits; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.statistics.StatisticsGateway; + +/** + * Collect metrics on EhCache caches, including detailed metrics on transactions and storage space. + * + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class EhCache2Metrics extends CacheMeterBinder { + + public EhCache2Metrics(Ehcache cache, Iterable tags) { + super(cache, cache.getName(), tags); + } + + /** + * Record metrics on an EhCache cache. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static Ehcache monitor(MeterRegistry registry, Ehcache cache, String... tags) { + return monitor(registry, cache, Tags.of(tags)); + } + + /** + * Record metrics on an EhCache cache. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static Ehcache monitor(MeterRegistry registry, Ehcache cache, Iterable tags) { + new EhCache2Metrics(cache, tags).bindTo(registry); + return cache; + } + + @Override + protected Long size() { + return getOrDefault(StatisticsGateway::getSize, null); + } + + @Override + protected long hitCount() { + return getOrDefault(StatisticsGateway::cacheHitCount, 0L); + } + + @Override + protected Long missCount() { + return getOrDefault(StatisticsGateway::cacheMissCount, null); + } + + @Override + protected Long evictionCount() { + return getOrDefault(StatisticsGateway::cacheEvictedCount, null); + } + + @Override + protected long putCount() { + return getOrDefault(StatisticsGateway::cachePutCount, 0L); + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + StatisticsGateway stats = getStats(); + Gauge.builder("cache.remoteSize", stats, StatisticsGateway::getRemoteSize) + .tags(getTagsWithCacheName()) + .description("The number of entries held remotely in this cache") + .register(registry); + + FunctionCounter.builder("cache.removals", stats, StatisticsGateway::cacheRemoveCount) + .tags(getTagsWithCacheName()) + .description("Cache removals") + .register(registry); + + FunctionCounter.builder("cache.puts.added", stats, StatisticsGateway::cachePutAddedCount) + .tags(getTagsWithCacheName()).tags("result", "added") + .description("Cache puts resulting in a new key/value pair") + .register(registry); + + FunctionCounter.builder("cache.puts.added", stats, StatisticsGateway::cachePutUpdatedCount) + .tags(getTagsWithCacheName()).tags("result", "updated") + .description("Cache puts resulting in an updated value") + .register(registry); + + missMetrics(registry); + commitTransactionMetrics(registry); + rollbackTransactionMetrics(registry); + recoveryTransactionMetrics(registry); + + Gauge.builder("cache.local.offheap.size", stats, StatisticsGateway::getLocalOffHeapSizeInBytes) + .tags(getTagsWithCacheName()) + .description("Local off-heap size") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("cache.local.heap.size", stats, StatisticsGateway::getLocalHeapSizeInBytes) + .tags(getTagsWithCacheName()) + .description("Local heap size") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("cache.local.disk.size", stats, StatisticsGateway::getLocalDiskSizeInBytes) + .tags(getTagsWithCacheName()) + .description("Local disk size") + .baseUnit(BaseUnits.BYTES) + .register(registry); + } + + @Nullable + private StatisticsGateway getStats() { + Ehcache cache = getCache(); + return cache != null ? cache.getStatistics() : null; + } + + private void missMetrics(MeterRegistry registry) { + StatisticsGateway stats = getStats(); + FunctionCounter.builder("cache.misses", stats, StatisticsGateway::cacheMissExpiredCount) + .tags(getTagsWithCacheName()) + .tags("reason", "expired") + .description("The number of times cache lookup methods have not returned a value, due to expiry") + .register(registry); + + FunctionCounter.builder("cache.misses", stats, StatisticsGateway::cacheMissNotFoundCount) + .tags(getTagsWithCacheName()) + .tags("reason", "notFound") + .description("The number of times cache lookup methods have not returned a value, because the key was not found") + .register(registry); + } + + private void commitTransactionMetrics(MeterRegistry registry) { + StatisticsGateway stats = getStats(); + FunctionCounter.builder("cache.xa.commits", stats, StatisticsGateway::xaCommitReadOnlyCount) + .tags(getTagsWithCacheName()) + .tags("result", "readOnly") + .description("Transaction commits that had a read-only result") + .register(registry); + + FunctionCounter.builder("cache.xa.commits", stats, StatisticsGateway::xaCommitExceptionCount) + .tags(getTagsWithCacheName()) + .tags("result", "exception") + .description("Transaction commits that failed") + .register(registry); + + FunctionCounter.builder("cache.xa.commits", stats, StatisticsGateway::xaCommitCommittedCount) + .tags(getTagsWithCacheName()) + .tags("result", "committed") + .description("Transaction commits that failed") + .register(registry); + } + + private void rollbackTransactionMetrics(MeterRegistry registry) { + StatisticsGateway stats = getStats(); + FunctionCounter.builder("cache.xa.rollbacks", stats, StatisticsGateway::xaRollbackExceptionCount) + .tags(getTagsWithCacheName()) + .tags("result", "exception") + .description("Transaction rollbacks that failed") + .register(registry); + + FunctionCounter.builder("cache.xa.rollbacks", stats, StatisticsGateway::xaRollbackSuccessCount) + .tags(getTagsWithCacheName()) + .tags("result", "success") + .description("Transaction rollbacks that failed") + .register(registry); + } + + private void recoveryTransactionMetrics(MeterRegistry registry) { + StatisticsGateway stats = getStats(); + FunctionCounter.builder("cache.xa.recoveries", stats, StatisticsGateway::xaRecoveryNothingCount) + .tags(getTagsWithCacheName()) + .tags("result", "nothing") + .description("Recovery transactions that recovered nothing") + .register(registry); + + FunctionCounter.builder("cache.xa.recoveries", stats, StatisticsGateway::xaRecoveryRecoveredCount) + .tags(getTagsWithCacheName()) + .tags("result", "success") + .description("Successful recovery transaction") + .register(registry); + } + + @Nullable + private Long getOrDefault(Function function, @Nullable Long defaultValue) { + StatisticsGateway ref = getStats(); + if (ref != null) { + return function.apply(ref); + } + + return defaultValue; + } + + private long getOrDefault(ToLongFunction function, long defaultValue) { + StatisticsGateway ref = getStats(); + if (ref != null) { + return function.applyAsLong(ref); + } + + return defaultValue; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/GuavaCacheMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/GuavaCacheMetrics.java new file mode 100644 index 0000000000..357baac0f2 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/GuavaCacheMetrics.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.ToLongFunction; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +/** + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class GuavaCacheMetrics> extends CacheMeterBinder { + /** + * Record metrics on a Guava cache. You must call {@link CacheBuilder#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName Will be used to tag metrics with "cache". + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + * @see com.google.common.cache.CacheStats + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, String... tags) { + return monitor(registry, cache, cacheName, Tags.of(tags)); + } + + /** + * Record metrics on a Guava cache. You must call {@link CacheBuilder#recordStats()} prior to building the cache + * for metrics to be recorded. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param cacheName The name prefix of the metrics. + * @param tags Tags to apply to all recorded metrics. + * @param Cache key type. + * @param Cache value type. + * @param The cache type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + * @see com.google.common.cache.CacheStats + */ + public static > C monitor(MeterRegistry registry, C cache, String cacheName, Iterable tags) { + new GuavaCacheMetrics<>(cache, cacheName, tags).bindTo(registry); + return cache; + } + + public GuavaCacheMetrics(C cache, String cacheName, Iterable tags) { + super(cache, cacheName, tags); + } + + @Override + protected Long size() { + return getOrDefault(Cache::size, null); + } + + @Override + protected long hitCount() { + return getOrDefault(c -> c.stats().hitCount(), 0L); + } + + @Override + protected Long missCount() { + return getOrDefault(c -> c.stats().missCount(), null); + } + + @Override + protected Long evictionCount() { + return getOrDefault(c -> c.stats().evictionCount(), null); + } + + @Override + protected long putCount() { + return getOrDefault(c -> c.stats().loadCount(), 0L); + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + C cache = getCache(); + if (cache instanceof LoadingCache) { + // dividing these gives you a measure of load latency + TimeGauge.builder("cache.load.duration", cache, TimeUnit.NANOSECONDS, c -> c.stats().totalLoadTime()) + .tags(getTagsWithCacheName()) + .description("The time the cache has spent loading new values") + .register(registry); + + FunctionCounter.builder("cache.load", cache, c -> c.stats().loadSuccessCount()) + .tags(getTagsWithCacheName()).tags("result", "success") + .description("The number of times cache lookup methods have successfully loaded a new value") + .register(registry); + + FunctionCounter.builder("cache.load", cache, c -> c.stats().loadExceptionCount()) + .tags(getTagsWithCacheName()).tags("result", "failure") + .description("The number of times cache lookup methods threw an exception while loading a new value") + .register(registry); + } + } + + @Nullable + private Long getOrDefault(Function, Long> function, @Nullable Long defaultValue) { + C ref = getCache(); + if (ref != null) { + return function.apply(ref); + } + + return defaultValue; + } + + private long getOrDefault(ToLongFunction> function, long defaultValue) { + C ref = getCache(); + if (ref != null) { + return function.applyAsLong(ref); + } + + return defaultValue; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastCacheMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastCacheMetrics.java new file mode 100644 index 0000000000..e270df5cf2 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastCacheMetrics.java @@ -0,0 +1,218 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.concurrent.TimeUnit; +import java.util.function.ToDoubleFunction; +import java.util.function.ToLongFunction; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.FunctionTimer; +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.BaseUnits; +import io.micrometer.binder.cache.HazelcastIMapAdapter.LocalMapStats; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +/** + * Collect metrics on Hazelcast caches, including detailed metrics on storage space, near cache usage, and timings. + * + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class HazelcastCacheMetrics extends CacheMeterBinder { + private final HazelcastIMapAdapter cache; + + /** + * Record metrics on a Hazelcast cache. + * + * @param registry registry to bind metrics to + * @param cache Hazelcast IMap cache to instrument + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static Object monitor(MeterRegistry registry, Object cache, String... tags) { + return monitor(registry, cache, Tags.of(tags)); + } + + /** + * Record metrics on a Hazelcast cache. + * + * @param registry registry to bind metrics to + * @param cache Hazelcast IMap cache to instrument + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static Object monitor(MeterRegistry registry, Object cache, Iterable tags) { + new HazelcastCacheMetrics(cache, tags).bindTo(registry); + return cache; + } + + /** + * Binder for Hazelcast cache metrics. + * + * @param cache Hazelcast IMap cache to instrument + * @param tags Tags to apply to all recorded metrics. + */ + public HazelcastCacheMetrics(Object cache, Iterable tags) { + super(cache, HazelcastIMapAdapter.nameOf(cache), tags); + this.cache = new HazelcastIMapAdapter(cache); + } + + @Override + protected Long size() { + LocalMapStats localMapStats = cache.getLocalMapStats(); + if ( localMapStats != null ) { + return localMapStats.getOwnedEntryCount(); + } + + return null; + } + + /** + * @return The number of hits against cache entries held in this local partition. Not all gets had to result from + * a get operation against {@link #cache}. If a get operation elsewhere in the cluster caused a lookup against an entry + * held in this partition, the hit will be recorded against map stats in this partition and not in the map stats of the + * calling {@code IMap}. + */ + @Override + protected long hitCount() { + LocalMapStats localMapStats = cache.getLocalMapStats(); + if (localMapStats != null) { + return localMapStats.getHits(); + } + + return 0L; + } + + /** + * @return There is no way to calculate miss count in Hazelcast. See issue #586. + */ + @Override + protected Long missCount() { + return null; + } + + @Nullable + @Override + protected Long evictionCount() { + return null; + } + + @Override + protected long putCount() { + LocalMapStats localMapStats = cache.getLocalMapStats(); + if (localMapStats != null) { + return localMapStats.getPutOperationCount(); + } + + return 0L; + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + Gauge.builder("cache.entries", cache, cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getBackupEntryCount)) + .tags(getTagsWithCacheName()).tag("ownership", "backup") + .description("The number of backup entries held by this member") + .register(registry); + + Gauge.builder("cache.entries", cache, cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getOwnedEntryCount)) + .tags(getTagsWithCacheName()).tag("ownership", "owned") + .description("The number of owned entries held by this member") + .register(registry); + + Gauge.builder("cache.entry.memory", cache, cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getBackupEntryMemoryCost)) + .tags(getTagsWithCacheName()).tag("ownership", "backup") + .description("Memory cost of backup entries held by this member") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("cache.entry.memory", cache, cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getOwnedEntryMemoryCost)) + .tags(getTagsWithCacheName()).tag("ownership", "owned") + .description("Memory cost of owned entries held by this member") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + FunctionCounter.builder("cache.partition.gets", cache, cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getGetOperationCount)) + .tags(getTagsWithCacheName()) + .description("The total number of get operations executed against this partition") + .register(registry); + + timings(registry); + nearCacheMetrics(registry); + } + + private double getDouble(LocalMapStats localMapStats, ToDoubleFunction function) { + return localMapStats != null ? function.applyAsDouble(localMapStats) : Double.NaN; + } + + private void nearCacheMetrics(MeterRegistry registry) { + LocalMapStats localMapStats = cache.getLocalMapStats(); + if (localMapStats != null && localMapStats.getNearCacheStats() != null) { + Gauge.builder("cache.near.requests", cache, cache -> getDouble(cache.getLocalMapStats(), (stats) -> stats.getNearCacheStats().getHits())) + .tags(getTagsWithCacheName()).tag("result", "hit") + .description("The number of hits (reads) of near cache entries owned by this member") + .register(registry); + + Gauge.builder("cache.near.requests", cache, cache -> getDouble(cache.getLocalMapStats(), (stats) -> stats.getNearCacheStats().getMisses())) + .tags(getTagsWithCacheName()).tag("result", "miss") + .description("The number of hits (reads) of near cache entries owned by this member") + .register(registry); + + Gauge.builder("cache.near.evictions", cache, cache -> getDouble(cache.getLocalMapStats(), (stats) -> stats.getNearCacheStats().getEvictions())) + .tags(getTagsWithCacheName()) + .description("The number of evictions of near cache entries owned by this member") + .register(registry); + + Gauge.builder("cache.near.persistences", cache, cache -> getDouble(cache.getLocalMapStats(), (stats) -> stats.getNearCacheStats().getPersistenceCount())) + .tags(getTagsWithCacheName()) + .description("The number of Near Cache key persistences (when the pre-load feature is enabled)") + .register(registry); + } + } + + private void timings(MeterRegistry registry) { + FunctionTimer.builder("cache.gets.latency", cache, + cache -> getLong(cache.getLocalMapStats(), LocalMapStats::getGetOperationCount), + cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getTotalGetLatency), TimeUnit.MILLISECONDS) + .tags(getTagsWithCacheName()) + .description("Cache gets") + .register(registry); + + FunctionTimer.builder("cache.puts.latency", cache, + cache -> getLong(cache.getLocalMapStats(), LocalMapStats::getPutOperationCount), + cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getTotalPutLatency), TimeUnit.MILLISECONDS) + .tags(getTagsWithCacheName()) + .description("Cache puts") + .register(registry); + + FunctionTimer.builder("cache.removals.latency", cache, + cache -> getLong(cache.getLocalMapStats(), LocalMapStats::getRemoveOperationCount), + cache -> getDouble(cache.getLocalMapStats(), LocalMapStats::getTotalRemoveLatency), TimeUnit.MILLISECONDS) + .tags(getTagsWithCacheName()) + .description("Cache removals") + .register(registry); + } + + private long getLong(LocalMapStats localMapStats, ToLongFunction function) { + return localMapStats != null ? function.applyAsLong(localMapStats) : 0L; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastIMapAdapter.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastIMapAdapter.java new file mode 100644 index 0000000000..0cfb369f69 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/HazelcastIMapAdapter.java @@ -0,0 +1,236 @@ +/* + * Copyright 2020 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.binder.cache; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.ref.WeakReference; + +import io.micrometer.core.lang.Nullable; + +import static java.lang.invoke.MethodType.methodType; + +/** + * Adapter for Hazelcast {@code IMap} class created to provide support for both Hazelcast 3 and Hazelcast 4 at the + * same time. Dynamically checks which Hazelcast version is on the classpath and resolves the right classes. + * + * @implNote Note that {@link MethodHandle} is used, so the performance does not suffer. + */ +class HazelcastIMapAdapter { + private static final Class CLASS_I_MAP = resolveOneOf("com.hazelcast.map.IMap", "com.hazelcast.core.IMap"); + private static final Class CLASS_LOCAL_MAP = resolveOneOf("com.hazelcast.map.LocalMapStats", "com.hazelcast.monitor.LocalMapStats"); + private static final Class CLASS_NEAR_CACHE_STATS = resolveOneOf("com.hazelcast.nearcache.NearCacheStats", "com.hazelcast.monitor.NearCacheStats"); + private static final MethodHandle GET_NAME; + private static final MethodHandle GET_LOCAL_MAP_STATS; + + static { + GET_NAME = resolveIMapMethod("getName", methodType(String.class)); + GET_LOCAL_MAP_STATS = resolveIMapMethod("getLocalMapStats", methodType(CLASS_LOCAL_MAP)); + } + + private final WeakReference cache; + + HazelcastIMapAdapter(Object cache) { + this.cache = new WeakReference<>(cache); + } + + static String nameOf(Object cache) { + try { + return (String) GET_NAME.invoke(cache); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + @Nullable + LocalMapStats getLocalMapStats() { + Object ref = cache.get(); + if (ref == null) { + return null; + } + + Object result = invoke(GET_LOCAL_MAP_STATS, ref); + return result == null ? null : new LocalMapStats(result); + } + + static class LocalMapStats { + private static final MethodHandle GET_NEAR_CACHE_STATS; + + private static final MethodHandle GET_OWNED_ENTRY_COUNT; + private static final MethodHandle GET_HITS; + private static final MethodHandle GET_PUT_OPERATION_COUNT; + private static final MethodHandle GET_BACKUP_ENTRY_COUNT; + private static final MethodHandle GET_BACKUP_ENTRY_MEMORY_COST; + private static final MethodHandle GET_OWNED_ENTRY_MEMORY_COST; + private static final MethodHandle GET_GET_OPERATION_COUNT; + private static final MethodHandle GET_TOTAL_GET_LATENCY; + private static final MethodHandle GET_TOTAL_PUT_LATENCY; + private static final MethodHandle GET_REMOVE_OPERATION_COUNT; + private static final MethodHandle GET_TOTAL_REMOVE_LATENCY; + + static { + GET_NEAR_CACHE_STATS = resolveMethod("getNearCacheStats", methodType(CLASS_NEAR_CACHE_STATS)); + + GET_OWNED_ENTRY_COUNT = resolveMethod("getOwnedEntryCount", methodType(long.class)); + GET_HITS = resolveMethod("getHits", methodType(long.class)); + GET_PUT_OPERATION_COUNT = resolveMethod("getPutOperationCount", methodType(long.class)); + GET_BACKUP_ENTRY_COUNT = resolveMethod("getBackupEntryCount", methodType(long.class)); + GET_BACKUP_ENTRY_MEMORY_COST = resolveMethod("getBackupEntryMemoryCost", methodType(long.class)); + GET_OWNED_ENTRY_MEMORY_COST = resolveMethod("getOwnedEntryMemoryCost", methodType(long.class)); + GET_GET_OPERATION_COUNT = resolveMethod("getGetOperationCount", methodType(long.class)); + GET_TOTAL_GET_LATENCY = resolveMethod("getTotalGetLatency", methodType(long.class)); + GET_TOTAL_PUT_LATENCY = resolveMethod("getTotalPutLatency", methodType(long.class)); + GET_REMOVE_OPERATION_COUNT = resolveMethod("getRemoveOperationCount", methodType(long.class)); + GET_TOTAL_REMOVE_LATENCY = resolveMethod("getTotalRemoveLatency", methodType(long.class)); + } + + private final Object localMapStats; + + LocalMapStats(Object localMapStats) { + this.localMapStats = localMapStats; + } + + long getOwnedEntryCount() { + return (long) invoke(GET_OWNED_ENTRY_COUNT, localMapStats); + } + + long getHits() { + return (long) invoke(GET_HITS, localMapStats); + } + + long getPutOperationCount() { + return (long) invoke(GET_PUT_OPERATION_COUNT, localMapStats); + } + + double getBackupEntryCount() { + return (long) invoke(GET_BACKUP_ENTRY_COUNT, localMapStats); + } + + long getBackupEntryMemoryCost() { + return (long) invoke(GET_BACKUP_ENTRY_MEMORY_COST, localMapStats); + } + + long getOwnedEntryMemoryCost() { + return (long) invoke(GET_OWNED_ENTRY_MEMORY_COST, localMapStats); + } + + long getGetOperationCount() { + return (long) invoke(GET_GET_OPERATION_COUNT, localMapStats); + } + + NearCacheStats getNearCacheStats() { + Object result = invoke(GET_NEAR_CACHE_STATS, localMapStats); + return result == null ? null : new NearCacheStats(result); + } + + long getTotalGetLatency() { + return (long) invoke(GET_TOTAL_GET_LATENCY, localMapStats); + } + + long getTotalPutLatency() { + return (long) invoke(GET_TOTAL_PUT_LATENCY, localMapStats); + } + + long getRemoveOperationCount() { + return (long) invoke(GET_REMOVE_OPERATION_COUNT, localMapStats); + } + + long getTotalRemoveLatency() { + return (long) invoke(GET_TOTAL_REMOVE_LATENCY, localMapStats); + } + + private static MethodHandle resolveMethod(String name, MethodType mt) { + try { + return MethodHandles.publicLookup().findVirtual(CLASS_LOCAL_MAP, name, mt); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + + static class NearCacheStats { + private static final MethodHandle GET_HITS; + private static final MethodHandle GET_MISSES; + private static final MethodHandle GET_EVICTIONS; + private static final MethodHandle GET_PERSISTENCE_COUNT; + + static { + GET_HITS = resolveMethod("getHits", methodType(long.class)); + GET_MISSES = resolveMethod("getMisses", methodType(long.class)); + GET_EVICTIONS = resolveMethod("getEvictions", methodType(long.class)); + GET_PERSISTENCE_COUNT = resolveMethod("getPersistenceCount", methodType(long.class)); + } + + private Object nearCacheStats; + + NearCacheStats(Object nearCacheStats) { + this.nearCacheStats = nearCacheStats; + } + + long getHits() { + return (long) invoke(GET_HITS, nearCacheStats); + } + + long getMisses() { + return (long) invoke(GET_MISSES, nearCacheStats); + } + + long getEvictions() { + return (long) invoke(GET_EVICTIONS, nearCacheStats); + } + + long getPersistenceCount() { + return (long) invoke(GET_PERSISTENCE_COUNT, nearCacheStats); + } + + private static MethodHandle resolveMethod(String name, MethodType mt) { + try { + return MethodHandles.publicLookup().findVirtual(CLASS_NEAR_CACHE_STATS, name, mt); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + + private static MethodHandle resolveIMapMethod(String name, MethodType mt) { + try { + return MethodHandles.publicLookup().findVirtual(CLASS_I_MAP, name, mt); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + private static Class resolveOneOf(String class1, String class2) { + try { + return Class.forName(class1); + } catch (ClassNotFoundException e) { + try { + return Class.forName(class2); + } catch (ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } + } + + private static Object invoke(MethodHandle mh, Object object) { + try { + return mh.invoke(object); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/JCacheMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/JCacheMetrics.java new file mode 100644 index 0000000000..ebe5d5c658 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/JCacheMetrics.java @@ -0,0 +1,159 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.List; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +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.config.InvalidConfigurationException; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +/** + * Collect metrics on JSR-107 JCache caches, including detailed metrics on manual puts and removals. + * See https://github.com/jsr107/demo/blob/master/src/test/java/javax/cache/core/StatisticsExample.java + *

+ * Note that JSR-107 does not provide any insight into the size or estimated size of the cache, so + * the size metric of a JCache cache will always report 0. + * + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class JCacheMetrics> extends CacheMeterBinder { + // VisibleForTesting + @Nullable + ObjectName objectName; + + /** + * Record metrics on a JCache cache. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + * @param The cache type. + * @param The cache key type. + * @param The cache value type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static > C monitor(MeterRegistry registry, C cache, String... tags) { + return monitor(registry, cache, Tags.of(tags)); + } + + /** + * Record metrics on a JCache cache. + * + * @param registry The registry to bind metrics to. + * @param cache The cache to instrument. + * @param tags Tags to apply to all recorded metrics. + * @param The cache type. + * @param The cache key type. + * @param The cache value type. + * @return The instrumented cache, unchanged. The original cache is not wrapped or proxied in any way. + */ + public static > C monitor(MeterRegistry registry, C cache, Iterable tags) { + new JCacheMetrics<>(cache, tags).bindTo(registry); + return cache; + } + + public JCacheMetrics(C cache, Iterable tags) { + super(cache, cache.getName(), tags); + try { + CacheManager cacheManager = cache.getCacheManager(); + if (cacheManager != null) { + String cacheManagerUri = cacheManager.getURI().toString() + .replace(':', '.'); // ehcache's uri is prefixed with 'urn:' + + this.objectName = new ObjectName("javax.cache:type=CacheStatistics" + + ",CacheManager=" + cacheManagerUri + + ",Cache=" + cache.getName()); + } + } catch (MalformedObjectNameException ignored) { + throw new InvalidConfigurationException("Cache name '" + cache.getName() + "' results in an invalid JMX name"); + } + } + + @Override + protected Long size() { + // JCache statistics don't support size + return null; + } + + @Override + protected long hitCount() { + return lookupStatistic("CacheHits"); + } + + @Override + protected Long missCount() { + return lookupStatistic("CacheMisses"); + } + + @Override + protected Long evictionCount() { + return lookupStatistic("CacheEvictions"); + } + + @Override + protected long putCount() { + return lookupStatistic("CachePuts"); + } + + @Override + protected void bindImplementationSpecificMetrics(MeterRegistry registry) { + if (objectName != null) { + Gauge.builder("cache.removals", objectName, objectName -> lookupStatistic("CacheRemovals")) + .tags(getTagsWithCacheName()) + .description("Cache removals") + .register(registry); + } + } + + private Long lookupStatistic(String name) { + if (objectName != null) { + try { + List mBeanServers = MBeanServerFactory.findMBeanServer(null); + for (MBeanServer mBeanServer : mBeanServers) { + try { + return (Long) mBeanServer.getAttribute(objectName, name); + } catch (AttributeNotFoundException | InstanceNotFoundException ex) { + // did not find MBean, try the next server + } + } + } catch (MBeanException | ReflectionException ex) { + throw new IllegalStateException(ex); + } + } + + // didn't find the MBean in any servers + return 0L; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/cache/package-info.java b/micrometer-binders/src/main/java/io/micrometer/binder/cache/package-info.java new file mode 100644 index 0000000000..c9514ec207 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/cache/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2017 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. + */ +@NonNullApi +package io.micrometer.binder.cache; + +import io.micrometer.core.lang.NonNullApi; diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/commonspool2/CommonsObjectPool2Metrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/commonspool2/CommonsObjectPool2Metrics.java new file mode 100644 index 0000000000..502a2d4b2d --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/commonspool2/CommonsObjectPool2Metrics.java @@ -0,0 +1,339 @@ +/* + * Copyright 2020 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.binder.commonspool2; + +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.ToDoubleFunction; + +import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanException; +import javax.management.MBeanServer; +import javax.management.MBeanServerDelegate; +import javax.management.MBeanServerFactory; +import javax.management.MBeanServerNotification; +import javax.management.MalformedObjectNameException; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.util.NamedThreadFactory; +import io.micrometer.core.lang.NonNull; +import io.micrometer.core.lang.Nullable; +import io.micrometer.core.util.internal.logging.InternalLogger; +import io.micrometer.core.util.internal.logging.InternalLoggerFactory; + +import static java.util.Collections.emptyList; + +/** + * Apache Commons Pool 2.x metrics collected from metrics exposed via the MBeanServer. + * Metrics are exposed for each object pool. + * + * @author Chao Chang + * @since 1.6.0 + */ +public class CommonsObjectPool2Metrics implements MeterBinder, AutoCloseable { + private static final InternalLogger log = InternalLoggerFactory.getInstance(CommonsObjectPool2Metrics.class); + private static final String JMX_DOMAIN = "org.apache.commons.pool2"; + private static final String METRIC_NAME_PREFIX = "commons.pool2."; + + private static final String[] TYPES = new String[]{"GenericObjectPool", "GenericKeyedObjectPool"}; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("commons-pool-metrics-updater")); + + private final MBeanServer mBeanServer; + private final Iterable tags; + private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>(); + + public CommonsObjectPool2Metrics() { + this(emptyList()); + } + + public CommonsObjectPool2Metrics(Iterable tags) { + this(getMBeanServer(), tags); + } + + public CommonsObjectPool2Metrics(MBeanServer mBeanServer, Iterable tags) { + this.mBeanServer = mBeanServer; + this.tags = tags; + } + + private static MBeanServer getMBeanServer() { + List mBeanServers = MBeanServerFactory.findMBeanServer(null); + if (!mBeanServers.isEmpty()) { + return mBeanServers.get(0); + } + return ManagementFactory.getPlatformMBeanServer(); + } + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + for (String type : TYPES) { + registerMetricsEventually( + type, + (o, tags) -> { + registerGaugeForObject(registry, o, + "NumIdle", "num.idle", tags, + "The number of instances currently idle in this pool", BaseUnits.OBJECTS); + registerGaugeForObject(registry, o, + "NumActive", "num.active", tags, + "The number of instances currently active in this pool", BaseUnits.OBJECTS); + registerGaugeForObject(registry, o, + "NumWaiters", "num.waiters", tags, + "The estimate of the number of threads currently blocked waiting for an object from the pool", + BaseUnits.THREADS); + + registerFunctionCounterForObject(registry, o, + "CreatedCount", "created", tags, + "The total number of objects created for this pool over the lifetime of the pool", + BaseUnits.OBJECTS); + registerFunctionCounterForObject(registry, o, + "BorrowedCount", "borrowed", tags, + "The total number of objects successfully borrowed from this pool over the lifetime of the pool", + BaseUnits.OBJECTS); + registerFunctionCounterForObject(registry, o, + "ReturnedCount", "returned", tags, + "The total number of objects returned to this pool over the lifetime of the pool", + BaseUnits.OBJECTS); + registerFunctionCounterForObject(registry, o, + "DestroyedCount", "destroyed", tags, + "The total number of objects destroyed by this pool over the lifetime of the pool", + BaseUnits.OBJECTS); + registerFunctionCounterForObject(registry, o, + "DestroyedByEvictorCount", "destroyed.by.evictor", tags, + "The total number of objects destroyed by the evictor associated with this pool over the lifetime of the pool", + BaseUnits.OBJECTS); + registerFunctionCounterForObject(registry, o, + "DestroyedByBorrowValidationCount", "destroyed.by.borrow.validation", tags, + "The total number of objects destroyed by this pool as a result of failing validation during borrowObject() over the lifetime of the pool", + BaseUnits.OBJECTS); + + registerTimeGaugeForObject(registry, o, + "MaxBorrowWaitTimeMillis", "max.borrow.wait", tags, + "The maximum time a thread has waited to borrow objects from the pool"); + registerTimeGaugeForObject(registry, o, + "MeanActiveTimeMillis", "mean.active", tags, + "The mean time objects are active"); + registerTimeGaugeForObject(registry, o, + "MeanIdleTimeMillis", "mean.idle", tags, + "The mean time objects are idle"); + registerTimeGaugeForObject(registry, o, + "MeanBorrowWaitTimeMillis", "mean.borrow.wait", tags, + "The mean time threads wait to borrow an object"); + }); + } + } + + private Iterable nameTag(ObjectName name, String type) + throws AttributeNotFoundException, MBeanException, ReflectionException, + InstanceNotFoundException { + Tags tags = Tags.of("name", name.getKeyProperty("name"), "type", type); + if (Objects.equals(type, "GenericObjectPool")) { + // for GenericObjectPool, we want to include the name and factoryType as tags + String factoryType = mBeanServer.getAttribute(name, "FactoryType").toString(); + tags = Tags.concat(tags, "factoryType", factoryType); + } + return tags; + } + + private void registerMetricsEventually(String type, BiConsumer perObject) { + try { + Set objs = + mBeanServer.queryNames(new ObjectName(JMX_DOMAIN + ":type=" + type + ",*"), null); + for (ObjectName o : objs) { + Iterable nameTags = emptyList(); + try { + nameTags = nameTag(o, type); + } catch (Exception e) { + log.error("exception in determining name tag", e); + } + perObject.accept(o, Tags.concat(tags, nameTags)); + } + } catch (MalformedObjectNameException e) { + throw new RuntimeException("Error registering commons pool2 based metrics", e); + } + + registerNotificationListener(type, perObject); + } + + /** + * This notification listener should remain indefinitely since new pools can be added at + * any time. + * + * @param type The pool type to listen for. + * @param perObject Metric registration handler when a new MBean is created. + */ + private void registerNotificationListener(String type, BiConsumer perObject) { + NotificationListener notificationListener = + // in notification listener, we cannot get attributes for the registered object, + // so we do it later time in a separate thread. + (notification, handback) -> { + executor.execute( + () -> { + MBeanServerNotification mbs = (MBeanServerNotification) notification; + ObjectName o = mbs.getMBeanName(); + Iterable nameTags = emptyList(); + int maxTries = 3; + for (int i = 0; i < maxTries; i++) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + try { + nameTags = nameTag(o, type); + break; + } catch (AttributeNotFoundException + | MBeanException + | ReflectionException + | InstanceNotFoundException e) { + if (i == maxTries - 1) { + log.error("can not set name tag", e); + } + } + } + perObject.accept(o, Tags.concat(tags, nameTags)); + }); + }; + + NotificationFilter filter = + (NotificationFilter) + notification -> { + if (!MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notification.getType())) + return false; + ObjectName obj = ((MBeanServerNotification) notification).getMBeanName(); + return obj.getDomain().equals(JMX_DOMAIN) && obj.getKeyProperty("type").equals(type); + }; + + try { + mBeanServer.addNotificationListener( + MBeanServerDelegate.DELEGATE_NAME, notificationListener, filter, null); + notificationListenerCleanUpRunnables.add( + () -> { + try { + mBeanServer.removeNotificationListener( + MBeanServerDelegate.DELEGATE_NAME, notificationListener); + } catch (InstanceNotFoundException | ListenerNotFoundException ignore) { + } + }); + } catch (InstanceNotFoundException ignore) { + // unable to register MBean listener + } + } + + @Override + public void close() { + notificationListenerCleanUpRunnables.forEach(Runnable::run); + executor.shutdown(); + } + + private void registerGaugeForObject( + MeterRegistry registry, + ObjectName o, + String jmxMetricName, + String meterName, + Tags allTags, + String description, + @Nullable String baseUnit) { + final AtomicReference gauge = new AtomicReference<>(); + gauge.set(Gauge + .builder( + METRIC_NAME_PREFIX + meterName, + mBeanServer, + getJmxAttribute(registry, gauge, o, jmxMetricName) + ) + .description(description) + .baseUnit(baseUnit) + .tags(allTags) + .register(registry) + ); + } + + private void registerFunctionCounterForObject(MeterRegistry registry, ObjectName o, String jmxMetricName, String meterName, Tags allTags, String description, @Nullable String baseUnit) { + final AtomicReference counter = new AtomicReference<>(); + counter.set(FunctionCounter + .builder( + METRIC_NAME_PREFIX + meterName, + mBeanServer, + getJmxAttribute(registry, counter, o, jmxMetricName) + ) + .description(description) + .baseUnit(baseUnit) + .tags(allTags) + .register(registry) + ); + } + + private void registerTimeGaugeForObject(MeterRegistry registry, ObjectName o, String jmxMetricName, + String meterName, Tags allTags, String description) { + final AtomicReference timeGauge = new AtomicReference<>(); + timeGauge.set(TimeGauge + .builder( + METRIC_NAME_PREFIX + meterName, + mBeanServer, + TimeUnit.MILLISECONDS, + getJmxAttribute(registry, timeGauge, o, jmxMetricName) + ) + .description(description) + .tags(allTags) + .register(registry) + ); + } + + private ToDoubleFunction getJmxAttribute( + MeterRegistry registry, + AtomicReference meter, + ObjectName o, + String jmxMetricName) { + return s -> safeDouble( + () -> { + if (!s.isRegistered(o)) { + registry.remove(meter.get()); + } + return s.getAttribute(o, jmxMetricName); + }); + } + + private double safeDouble(Callable callable) { + try { + return Double.parseDouble(callable.call().toString()); + } catch (Exception e) { + return Double.NaN; + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/db/DatabaseTableMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/db/DatabaseTableMetrics.java new file mode 100644 index 0000000000..1b63ff4ef0 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/db/DatabaseTableMetrics.java @@ -0,0 +1,124 @@ +/* + * Copyright 2017 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.binder.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.ToDoubleFunction; + +import javax.sql.DataSource; + +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class DatabaseTableMetrics implements MeterBinder { + private final DataSource dataSource; + private final String query; + private final String dataSourceName; + private final String tableName; + private final Iterable tags; + + /** + * Record the row count for an individual database table. + * + * @param dataSource The data source to use to run the row count query. + * @param dataSourceName Will be used to tag metrics with "db". + * @param tableName The name of the table to report table size for. + * @param tags Tags to apply to all recorded metrics. + */ + public DatabaseTableMetrics(DataSource dataSource, String dataSourceName, String tableName, Iterable tags) { + this(dataSource, "SELECT COUNT(1) FROM " + tableName, dataSourceName, tableName, tags); + } + + /** + * Record the result based on a query. + * + * @param dataSource The data source to use to run the row count query. + * @param query The query to be run against the table. The first column of the result will be the metric and + * it should return a single row. + * @param dataSourceName The name prefix of the metrics. + * @param tableName The name of the table to report table size for. + * @param tags Tags to apply to all recorded metrics. + */ + public DatabaseTableMetrics(DataSource dataSource, String query, String dataSourceName, String tableName, Iterable tags) { + this.dataSource = dataSource; + this.query = query; + this.dataSourceName = dataSourceName; + this.tableName = tableName; + this.tags = tags; + } + + /** + * Record the row count for an individual database table. + * + * @param registry The registry to bind metrics to. + * @param tableName The name of the table to report table size for. + * @param dataSourceName Will be used to tag metrics with "db". + * @param dataSource The data source to use to run the row count query. + * @param tags Tags to apply to all recorded metrics. Must be an even number of arguments representing key/value pairs of tags. + */ + public static void monitor(MeterRegistry registry, String tableName, String dataSourceName, DataSource dataSource, String... tags) { + monitor(registry, dataSource, dataSourceName, tableName, Tags.of(tags)); + } + + /** + * Record the row count for an individual database table. + * + * @param registry The registry to bind metrics to. + * @param dataSource The data source to use to run the row count query. + * @param dataSourceName The name prefix of the metrics. + * @param tableName The name of the table to report table size for. + * @param tags Tags to apply to all recorded metrics. + */ + public static void monitor(MeterRegistry registry, DataSource dataSource, String dataSourceName, String tableName, Iterable tags) { + new DatabaseTableMetrics(dataSource, dataSourceName, tableName, tags).bindTo(registry); + } + + @Override + public void bindTo(MeterRegistry registry) { + ToDoubleFunction totalRows = ds -> { + try (Connection conn = ds.getConnection(); + PreparedStatement ps = conn.prepareStatement(query); + ResultSet rs = ps.executeQuery()) { + rs.next(); + return rs.getInt(1); + } catch (SQLException ignored) { + return 0; + } + }; + + Gauge.builder("db.table.size", dataSource, totalRows) + .tags(tags) + .tag("db", dataSourceName) + .tag("table", tableName) + .description("Number of rows in a database table") + .baseUnit(BaseUnits.ROWS) + .register(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/db/JooqExecuteListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/db/JooqExecuteListener.java new file mode 100644 index 0000000000..6c138bbb2b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/db/JooqExecuteListener.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 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.binder.db; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.util.StringUtils; +import org.jooq.ExecuteContext; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DefaultExecuteListener; + +class JooqExecuteListener extends DefaultExecuteListener { + private final MeterRegistry registry; + private final Iterable tags; + private final Supplier> queryTagsSupplier; + + private final Object sampleLock = new Object(); + private final Map sampleByExecuteContext = new HashMap<>(); + + public JooqExecuteListener(MeterRegistry registry, Iterable tags, Supplier> queryTags) { + this.registry = registry; + this.tags = tags; + this.queryTagsSupplier = queryTags; + } + + @Override + public void start(ExecuteContext ctx) { + startTimer(ctx); + } + + @Override + public void executeStart(ExecuteContext ctx) { + startTimer(ctx); + } + + private void startTimer(ExecuteContext ctx) { + Timer.Sample started = Timer.start(registry); + synchronized (sampleLock) { + sampleByExecuteContext.put(ctx, started); + } + } + + @Override + public void executeEnd(ExecuteContext ctx) { + stopTimerIfStillRunning(ctx); + } + + @Override + public void end(ExecuteContext ctx) { + stopTimerIfStillRunning(ctx); + } + + private void stopTimerIfStillRunning(ExecuteContext ctx) { + Iterable queryTags = queryTagsSupplier.get(); + if (queryTags == null) return; + + Timer.Sample sample; + synchronized (sampleLock) { + sample = sampleByExecuteContext.remove(ctx); + } + if (sample == null) return; + + String exceptionName = "none"; + String exceptionSubclass = "none"; + + Exception exception = ctx.exception(); + if (exception != null) { + if (exception instanceof DataAccessException) { + DataAccessException dae = (DataAccessException) exception; + exceptionName = dae.sqlStateClass().name().toLowerCase().replace('_', ' '); + exceptionSubclass = dae.sqlStateSubclass().name().toLowerCase().replace('_', ' '); + if (exceptionSubclass.contains("no subclass")) { + exceptionSubclass = "none"; + } + } else { + String simpleName = exception.getClass().getSimpleName(); + exceptionName = StringUtils.isNotBlank(simpleName) ? simpleName : exception.getClass().getName(); + } + } + + //noinspection unchecked + sample.stop(Timer.builder("jooq.query") + .description("Execution time of a SQL query performed with JOOQ") + .tags(queryTags) + .tag("type", ctx.type().name().toLowerCase()) + .tag("exception", exceptionName) + .tag("exception.subclass", exceptionSubclass) + .tags(tags) + .register(registry)); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/db/MetricsDSLContext.java b/micrometer-binders/src/main/java/io/micrometer/binder/db/MetricsDSLContext.java new file mode 100644 index 0000000000..a9be6cd09e --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/db/MetricsDSLContext.java @@ -0,0 +1,4221 @@ +/* + * Copyright 2020 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.binder.db; + +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.jooq.Record; +import org.jooq.*; +import org.jooq.conf.Settings; +import org.jooq.exception.ConfigurationException; +import org.jooq.exception.DataAccessException; +import org.jooq.exception.InvalidResultException; +import org.jooq.exception.NoDataFoundException; +import org.jooq.exception.TooManyRowsException; +import org.jooq.impl.DSL; +import org.jooq.tools.jdbc.MockCallable; +import org.jooq.tools.jdbc.MockDataProvider; +import org.jooq.tools.jdbc.MockRunnable; +import org.jooq.util.xml.jaxb.InformationSchema; + +/** + * Time SQL queries passing through jOOQ. + * + * Timing of batch operations and with statements not supported. + *

+ * This can be used as the regular jOOQ {@link DSLContext} but queries will be timed + * and tags can be set for the query timed. For example: + *

+ * 
+ *     MetricsDSLContext jooq = MetricsDSLContext.withMetrics(DSL.using(configuration), meterRegistry, Tags.empty());
+ *     jooq.tag("name", "selectAllAuthors").select(asterisk()).from("author").fetch();
+ * 
+ * 
+ * + * This requires jOOQ 3.14.0 or later. + * + * @author Jon Schneider + * @author Johnny Lim + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +public class MetricsDSLContext implements DSLContext { + private final DSLContext context; + private final MeterRegistry registry; + private final Iterable tags; + private final ThreadLocal> contextTags = new ThreadLocal<>(); + + private final ExecuteListenerProvider defaultExecuteListenerProvider; + + public static MetricsDSLContext withMetrics(DSLContext jooq, MeterRegistry registry, Iterable tags) { + return new MetricsDSLContext(jooq, registry, tags); + } + + MetricsDSLContext(DSLContext context, MeterRegistry registry, Iterable tags) { + this.registry = registry; + this.tags = tags; + + this.defaultExecuteListenerProvider = () -> new JooqExecuteListener(registry, tags, () -> { + Iterable queryTags = contextTags.get(); + contextTags.remove(); + return queryTags; + }); + Configuration configuration = context.configuration().derive(); + Configuration derivedConfiguration = derive(configuration, this.defaultExecuteListenerProvider); + + this.context = DSL.using(derivedConfiguration); + } + + public Q time(Q q) { + q.attach(time(q.configuration())); + return q; + } + + public Configuration time(Configuration c) { + Iterable queryTags = contextTags.get(); + contextTags.remove(); + return derive(c, () -> new JooqExecuteListener(registry, tags, () -> queryTags)); + } + + private Configuration derive(Configuration configuration, ExecuteListenerProvider executeListenerProvider) { + ExecuteListenerProvider[] providers = configuration.executeListenerProviders(); + for (int i = 0; i < providers.length; i++) { + if (providers[i] == this.defaultExecuteListenerProvider) { + ExecuteListenerProvider[] newProviders = Arrays.copyOf(providers, providers.length); + newProviders[i] = executeListenerProvider; + return configuration.derive(newProviders); + } + } + ExecuteListenerProvider[] newProviders = Arrays.copyOf(providers, providers.length + 1); + newProviders[providers.length] = executeListenerProvider; + return configuration.derive(newProviders); + } + + @SuppressWarnings("unchecked") + public O timeCoercable(Object o) { + return (O) time((Query) o); + } + + public DSLContext tag(String key, String name) { + return tags(Tags.of(key, name)); + } + + public DSLContext tag(Tag tag) { + return tags(Tags.of(tag)); + } + + public DSLContext tags(Iterable tags) { + contextTags.set(tags); + return this; + } + + @Override + public Schema map(Schema schema) { + return context.map(schema); + } + + @Override + public Table map(Table table) { + return context.map(table); + } + + @Override + public Parser parser() { + return context.parser(); + } + + @Override + public Connection parsingConnection() { + return context.parsingConnection(); + } + + @Override + public DataSource parsingDataSource() { + return context.parsingDataSource(); + } + + @Override + public Connection diagnosticsConnection() { + return context.diagnosticsConnection(); + } + + @Override + public DataSource diagnosticsDataSource() { + return context.diagnosticsDataSource(); + } + + @Override + public Version version(String id) { + return context.version(id); + } + + @Override + public Migration migrateTo(Version to) { + return context.migrateTo(to); + } + + @Override + public Meta meta() { + return context.meta(); + } + + @Override + public Meta meta(DatabaseMetaData meta) { + return context.meta(meta); + } + + @Override + public Meta meta(Catalog... catalogs) { + return context.meta(catalogs); + } + + @Override + public Meta meta(Schema... schemas) { + return context.meta(schemas); + } + + @Override + public Meta meta(Table... tables) { + return context.meta(tables); + } + + @Override + public Meta meta(InformationSchema schema) { + return context.meta(schema); + } + + @Override + public Meta meta(String... sources) { + return context.meta(sources); + } + + @Override + @Internal + public Meta meta(Source... scripts) { + return context.meta(scripts); + } + + @Override + public Meta meta(Query... queries) { + return context.meta(queries); + } + + @Override + public InformationSchema informationSchema(Catalog catalog) { + return context.informationSchema(catalog); + } + + @Override + public InformationSchema informationSchema(Catalog... catalogs) { + return context.informationSchema(catalogs); + } + + @Override + public InformationSchema informationSchema(Schema schema) { + return context.informationSchema(schema); + } + + @Override + public InformationSchema informationSchema(Schema... schemas) { + return context.informationSchema(schemas); + } + + @Override + public InformationSchema informationSchema(Table table) { + return context.informationSchema(table); + } + + @Override + public InformationSchema informationSchema(Table... table) { + return context.informationSchema(table); + } + + @Override + public Explain explain(Query query) { + return context.explain(query); + } + + @Override + public T transactionResult(TransactionalCallable transactional) { + return context.transactionResult(transactional); + } + + @Override + public T transactionResult(ContextTransactionalCallable transactional) throws ConfigurationException { + return context.transactionResult(transactional); + } + + @Override + public void transaction(TransactionalRunnable transactional) { + context.transaction(transactional); + } + + @Override + public void transaction(ContextTransactionalRunnable transactional) throws ConfigurationException { + context.transaction(transactional); + } + + @Override + public CompletionStage transactionResultAsync(TransactionalCallable transactional) throws ConfigurationException { + return context.transactionResultAsync(transactional); + } + + @Override + public CompletionStage transactionAsync(TransactionalRunnable transactional) throws ConfigurationException { + return context.transactionAsync(transactional); + } + + @Override + public CompletionStage transactionResultAsync(Executor executor, TransactionalCallable transactional) throws ConfigurationException { + return context.transactionResultAsync(executor, transactional); + } + + @Override + public CompletionStage transactionAsync(Executor executor, TransactionalRunnable transactional) throws ConfigurationException { + return context.transactionAsync(executor, transactional); + } + + @Override + public T connectionResult(ConnectionCallable callable) { + return context.connectionResult(callable); + } + + @Override + public void connection(ConnectionRunnable runnable) { + context.connection(runnable); + } + + @Override + public T mockResult(MockDataProvider provider, MockCallable mockable) { + return context.mockResult(provider, mockable); + } + + @Override + public void mock(MockDataProvider provider, MockRunnable mockable) { + context.mock(provider, mockable); + } + + @Override + @Internal + @Deprecated + public RenderContext renderContext() { + return context.renderContext(); + } + + @Override + public String render(QueryPart part) { + return context.render(part); + } + + @Override + public String renderNamedParams(QueryPart part) { + return context.renderNamedParams(part); + } + + @Override + public String renderNamedOrInlinedParams(QueryPart part) { + return context.renderNamedOrInlinedParams(part); + } + + @Override + public String renderInlined(QueryPart part) { + return context.renderInlined(part); + } + + @Override + public List extractBindValues(QueryPart part) { + return context.extractBindValues(part); + } + + @Override + public Map> extractParams(QueryPart part) { + return context.extractParams(part); + } + + @Override + public Param extractParam(QueryPart part, String name) { + return context.extractParam(part, name); + } + + @Override + @Internal + @Deprecated + public BindContext bindContext(PreparedStatement stmt) { + return context.bindContext(stmt); + } + + @Override + @Deprecated + public int bind(QueryPart part, PreparedStatement stmt) { + return context.bind(part, stmt); + } + + @Override + public void attach(Attachable... attachables) { + context.attach(attachables); + } + + @Override + public void attach(Collection attachables) { + context.attach(attachables); + } + + @Override + public LoaderOptionsStep loadInto(Table table) { + return context.loadInto(table); + } + + @Override + public Queries queries(Query... queries) { + return context.queries(queries); + } + + @Override + public Queries queries(Collection queries) { + return context.queries(queries); + } + + @Override + public Block begin(Statement... statements) { + return context.begin(statements); + } + + @Override + public Block begin(Collection statements) { + return context.begin(statements); + } + + @Override + public RowCountQuery query(SQL sql) { + return context.query(sql); + } + + @Override + public RowCountQuery query(String sql) { + return context.query(sql); + } + + @Override + public RowCountQuery query(String sql, Object... bindings) { + return context.query(sql, bindings); + } + + @Override + public RowCountQuery query(String sql, QueryPart... parts) { + return context.query(sql, parts); + } + + @Override + public Result fetch(SQL sql) throws DataAccessException { + return context.fetch(sql); + } + + @Override + public Result fetch(String sql) throws DataAccessException { + return context.fetch(sql); + } + + @Override + public Result fetch(String sql, Object... bindings) throws DataAccessException { + return context.fetch(sql, bindings); + } + + @Override + public Result fetch(String sql, QueryPart... parts) throws DataAccessException { + return context.fetch(sql, parts); + } + + @Override + public Cursor fetchLazy(SQL sql) throws DataAccessException { + return context.fetchLazy(sql); + } + + @Override + public Cursor fetchLazy(String sql) throws DataAccessException { + return context.fetchLazy(sql); + } + + @Override + public Cursor fetchLazy(String sql, Object... bindings) throws DataAccessException { + return context.fetchLazy(sql, bindings); + } + + @Override + public Cursor fetchLazy(String sql, QueryPart... parts) throws DataAccessException { + return context.fetchLazy(sql, parts); + } + + @Override + public CompletionStage> fetchAsync(SQL sql) { + return context.fetchAsync(sql); + } + + @Override + public CompletionStage> fetchAsync(String sql) { + return context.fetchAsync(sql); + } + + @Override + public CompletionStage> fetchAsync(String sql, Object... bindings) { + return context.fetchAsync(sql, bindings); + } + + @Override + public CompletionStage> fetchAsync(String sql, QueryPart... parts) { + return context.fetchAsync(sql, parts); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, SQL sql) { + return context.fetchAsync(executor, sql); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, String sql) { + return context.fetchAsync(executor, sql); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, String sql, Object... bindings) { + return context.fetchAsync(executor, sql, bindings); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, String sql, QueryPart... parts) { + return context.fetchAsync(executor, sql, parts); + } + + @Override + public Stream fetchStream(SQL sql) throws DataAccessException { + return context.fetchStream(sql); + } + + @Override + public Stream fetchStream(String sql) throws DataAccessException { + return context.fetchStream(sql); + } + + @Override + public Stream fetchStream(String sql, Object... bindings) throws DataAccessException { + return context.fetchStream(sql, bindings); + } + + @Override + public Stream fetchStream(String sql, QueryPart... parts) throws DataAccessException { + return context.fetchStream(sql, parts); + } + + @Override + public Results fetchMany(SQL sql) throws DataAccessException { + return context.fetchMany(sql); + } + + @Override + public Results fetchMany(String sql) throws DataAccessException { + return context.fetchMany(sql); + } + + @Override + public Results fetchMany(String sql, Object... bindings) throws DataAccessException { + return context.fetchMany(sql, bindings); + } + + @Override + public Results fetchMany(String sql, QueryPart... parts) throws DataAccessException { + return context.fetchMany(sql, parts); + } + + @Override + public Record fetchOne(SQL sql) throws DataAccessException, TooManyRowsException { + return context.fetchOne(sql); + } + + @Override + public Record fetchOne(String sql) throws DataAccessException, TooManyRowsException { + return context.fetchOne(sql); + } + + @Override + public Record fetchOne(String sql, Object... bindings) throws DataAccessException, TooManyRowsException { + return context.fetchOne(sql, bindings); + } + + @Override + public Record fetchOne(String sql, QueryPart... parts) throws DataAccessException, TooManyRowsException { + return context.fetchOne(sql, parts); + } + + @Override + public Record fetchSingle(SQL sql) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(sql); + } + + @Override + public Record fetchSingle(String sql) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(sql); + } + + @Override + public Record fetchSingle(String sql, Object... bindings) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(sql, bindings); + } + + @Override + public Record fetchSingle(String sql, QueryPart... parts) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(sql, parts); + } + + @Override + public Optional fetchOptional(SQL sql) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(sql); + } + + @Override + public Optional fetchOptional(String sql) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(sql); + } + + @Override + public Optional fetchOptional(String sql, Object... bindings) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(sql, bindings); + } + + @Override + public Optional fetchOptional(String sql, QueryPart... parts) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(sql, parts); + } + + @Override + public Object fetchValue(SQL sql) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(sql); + } + + @Override + public Object fetchValue(String sql) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(sql); + } + + @Override + public Object fetchValue(String sql, Object... bindings) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(sql, bindings); + } + + @Override + public Object fetchValue(String sql, QueryPart... parts) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(sql, parts); + } + + @Override + public Optional fetchOptionalValue(SQL sql) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(sql); + } + + @Override + public Optional fetchOptionalValue(String sql) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(sql); + } + + @Override + public Optional fetchOptionalValue(String sql, Object... bindings) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(sql, bindings); + } + + @Override + public Optional fetchOptionalValue(String sql, QueryPart... parts) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(sql, parts); + } + + @Override + public List fetchValues(SQL sql) throws DataAccessException, InvalidResultException { + return context.fetchValues(sql); + } + + @Override + public List fetchValues(String sql) throws DataAccessException, InvalidResultException { + return context.fetchValues(sql); + } + + @Override + public List fetchValues(String sql, Object... bindings) throws DataAccessException, InvalidResultException { + return context.fetchValues(sql, bindings); + } + + @Override + public List fetchValues(String sql, QueryPart... parts) throws DataAccessException, InvalidResultException { + return context.fetchValues(sql, parts); + } + + @Override + public int execute(SQL sql) throws DataAccessException { + return context.execute(sql); + } + + @Override + public int execute(String sql) throws DataAccessException { + return context.execute(sql); + } + + @Override + public int execute(String sql, Object... bindings) throws DataAccessException { + return context.execute(sql, bindings); + } + + @Override + public int execute(String sql, QueryPart... parts) throws DataAccessException { + return context.execute(sql, parts); + } + + @Override + public ResultQuery resultQuery(SQL sql) { + return context.resultQuery(sql); + } + + @Override + public ResultQuery resultQuery(String sql) { + return context.resultQuery(sql); + } + + @Override + public ResultQuery resultQuery(String sql, Object... bindings) { + return context.resultQuery(sql, bindings); + } + + @Override + public ResultQuery resultQuery(String sql, QueryPart... parts) { + return context.resultQuery(sql, parts); + } + + @Override + public Result fetch(ResultSet rs) throws DataAccessException { + return context.fetch(rs); + } + + @Override + public Result fetch(ResultSet rs, Field... fields) throws DataAccessException { + return context.fetch(rs, fields); + } + + @Override + public Result fetch(ResultSet rs, DataType... types) throws DataAccessException { + return context.fetch(rs, types); + } + + @Override + public Result fetch(ResultSet rs, Class... types) throws DataAccessException { + return context.fetch(rs, types); + } + + @Override + public Record fetchOne(ResultSet rs) throws DataAccessException, TooManyRowsException { + return context.fetchOne(rs); + } + + @Override + public Record fetchOne(ResultSet rs, Field... fields) throws DataAccessException, TooManyRowsException { + return context.fetchOne(rs, fields); + } + + @Override + public Record fetchOne(ResultSet rs, DataType... types) throws DataAccessException, TooManyRowsException { + return context.fetchOne(rs, types); + } + + @Override + public Record fetchOne(ResultSet rs, Class... types) throws DataAccessException, TooManyRowsException { + return context.fetchOne(rs, types); + } + + @Override + public Record fetchSingle(ResultSet rs) throws DataAccessException, TooManyRowsException { + return context.fetchSingle(rs); + } + + @Override + public Record fetchSingle(ResultSet rs, Field... fields) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(rs, fields); + } + + @Override + public Record fetchSingle(ResultSet rs, DataType... types) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(rs, types); + } + + @Override + public Record fetchSingle(ResultSet rs, Class... types) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(rs, types); + } + + @Override + public Optional fetchOptional(ResultSet rs) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchOptional(rs); + } + + @Override + public Optional fetchOptional(ResultSet rs, Field... fields) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(rs, fields); + } + + @Override + public Optional fetchOptional(ResultSet rs, DataType... types) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(rs, types); + } + + @Override + public Optional fetchOptional(ResultSet rs, Class... types) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(rs, types); + } + + @Override + public Object fetchValue(ResultSet rs) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(rs); + } + + @Override + public T fetchValue(ResultSet rs, Field field) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(rs, field); + } + + @Override + public T fetchValue(ResultSet rs, DataType type) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(rs, type); + } + + @Override + public T fetchValue(ResultSet rs, Class type) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchValue(rs, type); + } + + @Override + public Optional fetchOptionalValue(ResultSet rs) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(rs); + } + + @Override + public Optional fetchOptionalValue(ResultSet rs, Field field) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(rs, field); + } + + @Override + public Optional fetchOptionalValue(ResultSet rs, DataType type) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(rs, type); + } + + @Override + public Optional fetchOptionalValue(ResultSet rs, Class type) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(rs, type); + } + + @Override + public List fetchValues(ResultSet rs) throws DataAccessException, InvalidResultException { + return context.fetchValues(rs); + } + + @Override + public List fetchValues(ResultSet rs, Field field) throws DataAccessException, InvalidResultException { + return context.fetchValues(rs, field); + } + + @Override + public List fetchValues(ResultSet rs, DataType type) throws DataAccessException, InvalidResultException { + return context.fetchValues(rs, type); + } + + @Override + public List fetchValues(ResultSet rs, Class type) throws DataAccessException, InvalidResultException { + return context.fetchValues(rs, type); + } + + @Override + public Cursor fetchLazy(ResultSet rs) throws DataAccessException { + return context.fetchLazy(rs); + } + + @Override + public Cursor fetchLazy(ResultSet rs, Field... fields) throws DataAccessException { + return context.fetchLazy(rs, fields); + } + + @Override + public Cursor fetchLazy(ResultSet rs, DataType... types) throws DataAccessException { + return context.fetchLazy(rs, types); + } + + @Override + public Cursor fetchLazy(ResultSet rs, Class... types) throws DataAccessException { + return context.fetchLazy(rs, types); + } + + @Override + public CompletionStage> fetchAsync(ResultSet rs) { + return context.fetchAsync(rs); + } + + @Override + public CompletionStage> fetchAsync(ResultSet rs, Field... fields) { + return context.fetchAsync(rs, fields); + } + + @Override + public CompletionStage> fetchAsync(ResultSet rs, DataType... types) { + return context.fetchAsync(rs, types); + } + + @Override + public CompletionStage> fetchAsync(ResultSet rs, Class... types) { + return context.fetchAsync(rs, types); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, ResultSet rs) { + return context.fetchAsync(executor, rs); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, ResultSet rs, Field... fields) { + return context.fetchAsync(executor, rs, fields); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, ResultSet rs, DataType... types) { + return context.fetchAsync(executor, rs, types); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, ResultSet rs, Class... types) { + return context.fetchAsync(executor, rs, types); + } + + @Override + public Stream fetchStream(ResultSet rs) throws DataAccessException { + return context.fetchStream(rs); + } + + @Override + public Stream fetchStream(ResultSet rs, Field... fields) throws DataAccessException { + return context.fetchStream(rs, fields); + } + + @Override + public Stream fetchStream(ResultSet rs, DataType... types) throws DataAccessException { + return context.fetchStream(rs, types); + } + + @Override + public Stream fetchStream(ResultSet rs, Class... types) throws DataAccessException { + return context.fetchStream(rs, types); + } + + @Override + public Result fetchFromTXT(String string) throws DataAccessException { + return context.fetchFromTXT(string); + } + + @Override + public Result fetchFromTXT(String string, String nullLiteral) throws DataAccessException { + return context.fetchFromTXT(string, nullLiteral); + } + + @Override + public Result fetchFromHTML(String string) throws DataAccessException { + return context.fetchFromHTML(string); + } + + @Override + public Result fetchFromCSV(String string) throws DataAccessException { + return context.fetchFromCSV(string); + } + + @Override + public Result fetchFromCSV(String string, char delimiter) throws DataAccessException { + return context.fetchFromCSV(string, delimiter); + } + + @Override + public Result fetchFromCSV(String string, boolean header) throws DataAccessException { + return context.fetchFromCSV(string, header); + } + + @Override + public Result fetchFromCSV(String string, boolean header, char delimiter) throws DataAccessException { + return context.fetchFromCSV(string, header, delimiter); + } + + @Override + public Result fetchFromJSON(String string) { + return context.fetchFromJSON(string); + } + + @Override + public Result fetchFromXML(String string) { + return context.fetchFromXML(string); + } + + @Override + public Result fetchFromStringData(String[]... data) { + return context.fetchFromStringData(data); + } + + @Override + public Result fetchFromStringData(List data) { + return context.fetchFromStringData(data); + } + + @Override + public Result fetchFromStringData(List data, boolean header) { + return context.fetchFromStringData(data, header); + } + + @Override + public WithAsStep with(String alias) { + return context.with(alias); + } + + @Override + public WithAsStep with(String alias, String... fieldAliases) { + return context.with(alias, fieldAliases); + } + + @Override + public WithAsStep with(String alias, Collection fieldAliases) { + return context.with(alias, fieldAliases); + } + + @Override + public WithAsStep with(Name alias) { + return context.with(alias); + } + + @Override + public WithAsStep with(Name alias, Name... fieldAliases) { + return context.with(alias, fieldAliases); + } + + @Override + public WithAsStep with(Name alias, Collection fieldAliases) { + return context.with(alias, fieldAliases); + } + + @SuppressWarnings("deprecation") + @Override + public WithAsStep with(String alias, Function, ? extends String> fieldNameFunction) { + return context.with(alias, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public WithAsStep with(String alias, BiFunction, ? super Integer, ? extends String> fieldNameFunction) { + return context.with(alias, fieldNameFunction); + } + + @Override + public WithAsStep1 with(String alias, String fieldAlias1) { + return context.with(alias, fieldAlias1); + } + + @Override + public WithAsStep2 with(String alias, String fieldAlias1, String fieldAlias2) { + return context.with(alias, fieldAlias1, fieldAlias2); + } + + @Override + public WithAsStep3 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3); + } + + @Override + public WithAsStep4 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4); + } + + @Override + public WithAsStep5 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5); + } + + @Override + public WithAsStep6 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6); + } + + @Override + public WithAsStep7 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7); + } + + @Override + public WithAsStep8 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8); + } + + @Override + public WithAsStep9 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9); + } + + @Override + public WithAsStep10 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10); + } + + @Override + public WithAsStep11 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11); + } + + @Override + public WithAsStep12 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12); + } + + @Override + public WithAsStep13 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13); + } + + @Override + public WithAsStep14 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14); + } + + @Override + public WithAsStep15 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15); + } + + @Override + public WithAsStep16 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16); + } + + @Override + public WithAsStep17 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17); + } + + @Override + public WithAsStep18 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18); + } + + @Override + public WithAsStep19 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19); + } + + @Override + public WithAsStep20 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20); + } + + @Override + public WithAsStep21 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20, String fieldAlias21) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21); + } + + @Override + public WithAsStep22 with(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20, String fieldAlias21, String fieldAlias22) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21, fieldAlias22); + } + + @Override + public WithAsStep1 with(Name alias, Name fieldAlias1) { + return context.with(alias, fieldAlias1); + } + + @Override + public WithAsStep2 with(Name alias, Name fieldAlias1, Name fieldAlias2) { + return context.with(alias, fieldAlias1, fieldAlias2); + } + + @Override + public WithAsStep3 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3); + } + + @Override + public WithAsStep4 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4); + } + + @Override + public WithAsStep5 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5); + } + + @Override + public WithAsStep6 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6); + } + + @Override + public WithAsStep7 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7); + } + + @Override + public WithAsStep8 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8); + } + + @Override + public WithAsStep9 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9); + } + + @Override + public WithAsStep10 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10); + } + + @Override + public WithAsStep11 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11); + } + + @Override + public WithAsStep12 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12); + } + + @Override + public WithAsStep13 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13); + } + + @Override + public WithAsStep14 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14); + } + + @Override + public WithAsStep15 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15); + } + + @Override + public WithAsStep16 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16); + } + + @Override + public WithAsStep17 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17); + } + + @Override + public WithAsStep18 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18); + } + + @Override + public WithAsStep19 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19); + } + + @Override + public WithAsStep20 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20); + } + + @Override + public WithAsStep21 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20, Name fieldAlias21) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21); + } + + @Override + public WithAsStep22 with(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20, Name fieldAlias21, Name fieldAlias22) { + return context.with(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21, fieldAlias22); + } + + @Override + public WithStep with(CommonTableExpression... tables) { + return context.with(tables); + } + + @Override + public WithStep with(Collection> tables) { + return context.with(tables); + } + + @Override + public WithAsStep withRecursive(String alias) { + return context.withRecursive(alias); + } + + @Override + public WithAsStep withRecursive(String alias, String... fieldAliases) { + return context.withRecursive(alias, fieldAliases); + } + + @Override + public WithAsStep withRecursive(String alias, Collection fieldAliases) { + return context.withRecursive(alias, fieldAliases); + } + + @Override + public WithAsStep withRecursive(Name alias) { + return context.withRecursive(alias); + } + + @Override + public WithAsStep withRecursive(Name alias, Name... fieldAliases) { + return context.withRecursive(alias, fieldAliases); + } + + @Override + public WithAsStep withRecursive(Name alias, Collection fieldAliases) { + return context.withRecursive(alias, fieldAliases); + } + + @SuppressWarnings("deprecation") + @Override + public WithAsStep withRecursive(String alias, Function, ? extends String> fieldNameFunction) { + return context.withRecursive(alias, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public WithAsStep withRecursive(String alias, BiFunction, ? super Integer, ? extends String> fieldNameFunction) { + return context.withRecursive(alias, fieldNameFunction); + } + + @Override + public WithAsStep1 withRecursive(String alias, String fieldAlias1) { + return context.withRecursive(alias, fieldAlias1); + } + + @Override + public WithAsStep2 withRecursive(String alias, String fieldAlias1, String fieldAlias2) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2); + } + + @Override + public WithAsStep3 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3); + } + + @Override + public WithAsStep4 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4); + } + + @Override + public WithAsStep5 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5); + } + + @Override + public WithAsStep6 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6); + } + + @Override + public WithAsStep7 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7); + } + + @Override + public WithAsStep8 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8); + } + + @Override + public WithAsStep9 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9); + } + + @Override + public WithAsStep10 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10); + } + + @Override + public WithAsStep11 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11); + } + + @Override + public WithAsStep12 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12); + } + + @Override + public WithAsStep13 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13); + } + + @Override + public WithAsStep14 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14); + } + + @Override + public WithAsStep15 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15); + } + + @Override + public WithAsStep16 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16); + } + + @Override + public WithAsStep17 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17); + } + + @Override + public WithAsStep18 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18); + } + + @Override + public WithAsStep19 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19); + } + + @Override + public WithAsStep20 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20); + } + + @Override + public WithAsStep21 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20, String fieldAlias21) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21); + } + + @Override + public WithAsStep22 withRecursive(String alias, String fieldAlias1, String fieldAlias2, String fieldAlias3, String fieldAlias4, String fieldAlias5, String fieldAlias6, String fieldAlias7, String fieldAlias8, String fieldAlias9, String fieldAlias10, String fieldAlias11, String fieldAlias12, String fieldAlias13, String fieldAlias14, String fieldAlias15, String fieldAlias16, String fieldAlias17, String fieldAlias18, String fieldAlias19, String fieldAlias20, String fieldAlias21, String fieldAlias22) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21, fieldAlias22); + } + + @Override + public WithAsStep1 withRecursive(Name alias, Name fieldAlias1) { + return context.withRecursive(alias, fieldAlias1); + } + + @Override + public WithAsStep2 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2); + } + + @Override + public WithAsStep3 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3); + } + + @Override + public WithAsStep4 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4); + } + + @Override + public WithAsStep5 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5); + } + + @Override + public WithAsStep6 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6); + } + + @Override + public WithAsStep7 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7); + } + + @Override + public WithAsStep8 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8); + } + + @Override + public WithAsStep9 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9); + } + + @Override + public WithAsStep10 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10); + } + + @Override + public WithAsStep11 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11); + } + + @Override + public WithAsStep12 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12); + } + + @Override + public WithAsStep13 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13); + } + + @Override + public WithAsStep14 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14); + } + + @Override + public WithAsStep15 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15); + } + + @Override + public WithAsStep16 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16); + } + + @Override + public WithAsStep17 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17); + } + + @Override + public WithAsStep18 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18); + } + + @Override + public WithAsStep19 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19); + } + + @Override + public WithAsStep20 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20); + } + + @Override + public WithAsStep21 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20, Name fieldAlias21) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21); + } + + @Override + public WithAsStep22 withRecursive(Name alias, Name fieldAlias1, Name fieldAlias2, Name fieldAlias3, Name fieldAlias4, Name fieldAlias5, Name fieldAlias6, Name fieldAlias7, Name fieldAlias8, Name fieldAlias9, Name fieldAlias10, Name fieldAlias11, Name fieldAlias12, Name fieldAlias13, Name fieldAlias14, Name fieldAlias15, Name fieldAlias16, Name fieldAlias17, Name fieldAlias18, Name fieldAlias19, Name fieldAlias20, Name fieldAlias21, Name fieldAlias22) { + return context.withRecursive(alias, fieldAlias1, fieldAlias2, fieldAlias3, fieldAlias4, fieldAlias5, fieldAlias6, fieldAlias7, fieldAlias8, fieldAlias9, fieldAlias10, fieldAlias11, fieldAlias12, fieldAlias13, fieldAlias14, fieldAlias15, fieldAlias16, fieldAlias17, fieldAlias18, fieldAlias19, fieldAlias20, fieldAlias21, fieldAlias22); + } + + @Override + public WithStep withRecursive(CommonTableExpression... tables) { + return context.withRecursive(tables); + } + + @Override + public WithStep withRecursive(Collection> tables) { + return context.withRecursive(tables); + } + + @Override + public SelectWhereStep selectFrom(Table table) { + return time(context.selectFrom(table)); + } + + @Override + public SelectWhereStep selectFrom(Name table) { + return time(context.selectFrom(table)); + } + + @Override + public SelectWhereStep selectFrom(SQL sql) { + return time(context.selectFrom(sql)); + } + + @Override + public SelectWhereStep selectFrom(String sql) { + return time(context.selectFrom(sql)); + } + + @Override + public SelectWhereStep selectFrom(String sql, Object... bindings) { + return time(context.selectFrom(sql, bindings)); + } + + @Override + public SelectWhereStep selectFrom(String sql, QueryPart... parts) { + return time(context.selectFrom(sql, parts)); + } + + @Override + public SelectSelectStep select(Collection fields) { + return time(context.select(fields)); + } + + @Override + public SelectSelectStep select(SelectFieldOrAsterisk... fields) { + return time(context.select(fields)); + } + + @Override + public SelectSelectStep> select(SelectField field1) { + return time(context.select(field1)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2) { + return time(context.select(field1, field2)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3) { + return time(context.select(field1, field2, field3)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4) { + return time(context.select(field1, field2, field3, field4)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5) { + return time(context.select(field1, field2, field3, field4, field5)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6) { + return time(context.select(field1, field2, field3, field4, field5, field6)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21)); + } + + @Override + public SelectSelectStep> select(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21, SelectField field22) { + return time(context.select(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22)); + } + + @Override + public SelectSelectStep selectDistinct(Collection fields) { + return time(context.selectDistinct(fields)); + } + + @Override + public SelectSelectStep selectDistinct(SelectFieldOrAsterisk... fields) { + return time(context.selectDistinct(fields)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1) { + return time(context.selectDistinct(field1)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2) { + return time(context.selectDistinct(field1, field2)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3) { + return time(context.selectDistinct(field1, field2, field3)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4) { + return time(context.selectDistinct(field1, field2, field3, field4)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5) { + return time(context.selectDistinct(field1, field2, field3, field4, field5)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21)); + } + + @Override + public SelectSelectStep> selectDistinct(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21, SelectField field22) { + return time(context.selectDistinct(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22)); + } + + @Override + public SelectSelectStep> selectZero() { + return time(context.selectZero()); + } + + @Override + public SelectSelectStep> selectOne() { + return time(context.selectOne()); + } + + @Override + public SelectSelectStep> selectCount() { + return time(context.selectCount()); + } + + @Override + public SelectQuery selectQuery() { + return time(context.selectQuery()); + } + + @Override + public SelectQuery selectQuery(TableLike table) { + return time(context.selectQuery(table)); + } + + @Override + public InsertQuery insertQuery(Table into) { + return time(context.insertQuery(into)); + } + + @Override + public InsertSetStep insertInto(Table into) { + return timeCoercable(context.insertInto(into)); + } + + @Override + public InsertValuesStep1 insertInto(Table into, Field field1) { + return time(context.insertInto(into, field1)); + } + + @Override + public InsertValuesStep2 insertInto(Table into, Field field1, Field field2) { + return time(context.insertInto(into, field1, field2)); + } + + @Override + public InsertValuesStep3 insertInto(Table into, Field field1, Field field2, Field field3) { + return time(context.insertInto(into, field1, field2, field3)); + } + + @Override + public InsertValuesStep4 insertInto(Table into, Field field1, Field field2, Field field3, Field field4) { + return time(context.insertInto(into, field1, field2, field3, field4)); + } + + @Override + public InsertValuesStep5 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5) { + return time(context.insertInto(into, field1, field2, field3, field4, field5)); + } + + @Override + public InsertValuesStep6 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6)); + } + + @Override + public InsertValuesStep7 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7)); + } + + @Override + public InsertValuesStep8 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8)); + } + + @Override + public InsertValuesStep9 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9)); + } + + @Override + public InsertValuesStep10 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10)); + } + + @Override + public InsertValuesStep11 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11)); + } + + @Override + public InsertValuesStep12 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12)); + } + + @Override + public InsertValuesStep13 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13)); + } + + @Override + public InsertValuesStep14 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14)); + } + + @Override + public InsertValuesStep15 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15)); + } + + @Override + public InsertValuesStep16 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16)); + } + + @Override + public InsertValuesStep17 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17)); + } + + @Override + public InsertValuesStep18 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18)); + } + + @Override + public InsertValuesStep19 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19)); + } + + @Override + public InsertValuesStep20 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20)); + } + + @Override + public InsertValuesStep21 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21)); + } + + @Override + public InsertValuesStep22 insertInto(Table into, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21, Field field22) { + return time(context.insertInto(into, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22)); + } + + @Override + public InsertValuesStepN insertInto(Table into, Field... fields) { + return time(context.insertInto(into, fields)); + } + + @Override + public InsertValuesStepN insertInto(Table into, Collection> fields) { + return time(context.insertInto(into, fields)); + } + + @Override + public UpdateQuery updateQuery(Table table) { + return time(context.updateQuery(table)); + } + + @Override + public UpdateSetFirstStep update(Table table) { + return timeCoercable(context.update(table)); + } + + @Override + public MergeUsingStep mergeInto(Table table) { + return context.mergeInto(table); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep1 mergeInto(Table table, Field field1) { + return context.mergeInto(table, field1); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep2 mergeInto(Table table, Field field1, Field field2) { + return context.mergeInto(table, field1, field2); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep3 mergeInto(Table table, Field field1, Field field2, Field field3) { + return context.mergeInto(table, field1, field2, field3); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep4 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4) { + return context.mergeInto(table, field1, field2, field3, field4); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep5 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5) { + return context.mergeInto(table, field1, field2, field3, field4, field5); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep6 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep7 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep8 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep9 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep10 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep11 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep12 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep13 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep14 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep15 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep16 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep17 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep18 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep19 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep20 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep21 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStep22 mergeInto(Table table, Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21, Field field22) { + return context.mergeInto(table, field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStepN mergeInto(Table table, Field... fields) { + return context.mergeInto(table, fields); + } + + @SuppressWarnings("deprecation") + @Override + public MergeKeyStepN mergeInto(Table table, Collection> fields) { + return context.mergeInto(table, fields); + } + + @Override + public DeleteQuery deleteQuery(Table table) { + return context.deleteQuery(table); + } + + @Override + public DeleteUsingStep deleteFrom(Table table) { + return context.deleteFrom(table); + } + + @Override + public DeleteUsingStep delete(Table table) { + return context.delete(table); + } + + @Override + public void batched(BatchedRunnable runnable) { + context.batched(runnable); + } + + @Override + public T batchedResult(BatchedCallable callable) { + return context.batchedResult(callable); + } + + @Override + public Batch batch(Query... queries) { + return context.batch(queries); + } + + @Override + public Batch batch(Queries queries) { + return context.batch(queries); + } + + @Override + public Batch batch(String... queries) { + return context.batch(queries); + } + + @Override + public Batch batch(Collection queries) { + return context.batch(queries); + } + + @Override + public BatchBindStep batch(Query query) { + return context.batch(query); + } + + @Override + public BatchBindStep batch(String sql) { + return context.batch(sql); + } + + @Override + public Batch batch(Query query, Object[]... bindings) { + return context.batch(query, bindings); + } + + @Override + public Batch batch(String sql, Object[]... bindings) { + return context.batch(sql, bindings); + } + + @Override + public Batch batchStore(UpdatableRecord... records) { + return context.batchStore(records); + } + + @Override + public Batch batchStore(Collection> records) { + return context.batchStore(records); + } + + @Override + public Batch batchInsert(TableRecord... records) { + return context.batchInsert(records); + } + + @Override + public Batch batchInsert(Collection> records) { + return context.batchInsert(records); + } + + @Override + public Batch batchUpdate(UpdatableRecord... records) { + return context.batchUpdate(records); + } + + @Override + public Batch batchUpdate(Collection> records) { + return context.batchUpdate(records); + } + + @Override + public Batch batchMerge(UpdatableRecord... records) { + return context.batchMerge(records); + } + + @Override + public Batch batchMerge(Collection> records) { + return context.batchMerge(records); + } + + @Override + public Batch batchDelete(UpdatableRecord... records) { + return context.batchDelete(records); + } + + @Override + public Batch batchDelete(Collection> records) { + return context.batchDelete(records); + } + + @Override + public Queries ddl(Catalog catalog) { + return context.ddl(catalog); + } + + @Override + public Queries ddl(Catalog schema, DDLExportConfiguration configuration) { + return context.ddl(schema, configuration); + } + + @Override + public Queries ddl(Catalog schema, DDLFlag... flags) { + return context.ddl(schema, flags); + } + + @Override + public Queries ddl(Schema schema) { + return context.ddl(schema); + } + + @Override + public Queries ddl(Schema schema, DDLExportConfiguration configuration) { + return context.ddl(schema, configuration); + } + + @Override + public Queries ddl(Schema schema, DDLFlag... flags) { + return context.ddl(schema, flags); + } + + @Override + public Queries ddl(Table table) { + return context.ddl(table); + } + + @Override + public Queries ddl(Table table, DDLExportConfiguration configuration) { + return context.ddl(table, configuration); + } + + @Override + public Queries ddl(Table table, DDLFlag... flags) { + return context.ddl(table, flags); + } + + @Override + public Queries ddl(Table... tables) { + return context.ddl(tables); + } + + @Override + public Queries ddl(Table[] tables, DDLExportConfiguration configuration) { + return context.ddl(tables, configuration); + } + + @Override + public Queries ddl(Table[] tables, DDLFlag... flags) { + return context.ddl(tables, flags); + } + + @Override + public Queries ddl(Collection> tables) { + return context.ddl(tables); + } + + @Override + public Queries ddl(Collection> tables, DDLFlag... flags) { + return context.ddl(tables, flags); + } + + @Override + public Queries ddl(Collection> tables, DDLExportConfiguration configuration) { + return context.ddl(tables, configuration); + } + + @Override + public RowCountQuery setCatalog(String catalog) { + return context.setCatalog(catalog); + } + + @Override + public RowCountQuery setCatalog(Name catalog) { + return context.setCatalog(catalog); + } + + @Override + public RowCountQuery setCatalog(Catalog catalog) { + return context.setCatalog(catalog); + } + + @Override + public RowCountQuery setSchema(String schema) { + return context.setSchema(schema); + } + + @Override + public RowCountQuery setSchema(Name schema) { + return context.setSchema(schema); + } + + @Override + public RowCountQuery setSchema(Schema schema) { + return context.setSchema(schema); + } + + @Override + public RowCountQuery set(Name name, Param param) { + return context.set(name, param); + } + + @Override + public CreateDatabaseFinalStep createDatabase(String database) { + return context.createDatabase(database); + } + + @Override + public CreateDatabaseFinalStep createDatabase(Name database) { + return context.createDatabase(database); + } + + @Override + public CreateDatabaseFinalStep createDatabase(Catalog database) { + return context.createDatabase(database); + } + + @Override + public CreateDatabaseFinalStep createDatabaseIfNotExists(String database) { + return context.createDatabaseIfNotExists(database); + } + + @Override + public CreateDatabaseFinalStep createDatabaseIfNotExists(Name database) { + return context.createDatabaseIfNotExists(database); + } + + @Override + public CreateDatabaseFinalStep createDatabaseIfNotExists(Catalog database) { + return context.createDatabaseIfNotExists(database); + } + + @Override + public CreateDomainAsStep createDomain(String domain) { + return context.createDomain(domain); + } + + @Override + public CreateDomainAsStep createDomain(Name domain) { + return context.createDomain(domain); + } + + @Override + public CreateDomainAsStep createDomain(Domain domain) { + return context.createDomain(domain); + } + + @Override + public CreateDomainAsStep createDomainIfNotExists(String domain) { + return context.createDomainIfNotExists(domain); + } + + @Override + public CreateDomainAsStep createDomainIfNotExists(Name domain) { + return context.createDomainIfNotExists(domain); + } + + @Override + public CreateDomainAsStep createDomainIfNotExists(Domain domain) { + return context.createDomainIfNotExists(domain); + } + + @Override + public CommentOnIsStep commentOnTable(String tableName) { + return context.commentOnTable(tableName); + } + + @Override + public CommentOnIsStep commentOnTable(Name tableName) { + return context.commentOnTable(tableName); + } + + @Override + public CommentOnIsStep commentOnTable(Table table) { + return context.commentOnTable(table); + } + + @Override + public CommentOnIsStep commentOnView(String viewName) { + return context.commentOnView(viewName); + } + + @Override + public CommentOnIsStep commentOnView(Name viewName) { + return context.commentOnView(viewName); + } + + @Override + public CommentOnIsStep commentOnView(Table view) { + return context.commentOnView(view); + } + + @Override + public CommentOnIsStep commentOnColumn(Name columnName) { + return context.commentOnColumn(columnName); + } + + @Override + public CommentOnIsStep commentOnColumn(Field field) { + return context.commentOnColumn(field); + } + + @Override + public CreateSchemaFinalStep createSchema(String schema) { + return context.createSchema(schema); + } + + @Override + public CreateSchemaFinalStep createSchema(Name schema) { + return context.createSchema(schema); + } + + @Override + public CreateSchemaFinalStep createSchema(Schema schema) { + return context.createSchema(schema); + } + + @Override + public CreateSchemaFinalStep createSchemaIfNotExists(String schema) { + return context.createSchemaIfNotExists(schema); + } + + @Override + public CreateSchemaFinalStep createSchemaIfNotExists(Name schema) { + return context.createSchemaIfNotExists(schema); + } + + @Override + public CreateSchemaFinalStep createSchemaIfNotExists(Schema schema) { + return context.createSchemaIfNotExists(schema); + } + + @Override + public CreateTableColumnStep createTable(String table) { + return context.createTable(table); + } + + @Override + public CreateTableColumnStep createTable(Name table) { + return context.createTable(table); + } + + @Override + public CreateTableColumnStep createTable(Table table) { + return context.createTable(table); + } + + @Override + public CreateTableColumnStep createTableIfNotExists(String table) { + return context.createTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createTableIfNotExists(Name table) { + return context.createTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createTableIfNotExists(Table table) { + return context.createTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createTemporaryTable(String table) { + return context.createTemporaryTable(table); + } + + @Override + public CreateTableColumnStep createTemporaryTable(Name table) { + return context.createTemporaryTable(table); + } + + @Override + public CreateTableColumnStep createTemporaryTable(Table table) { + return context.createTemporaryTable(table); + } + + @Override + public CreateTableColumnStep createTemporaryTableIfNotExists(String table) { + return context.createTemporaryTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createTemporaryTableIfNotExists(Name table) { + return context.createTemporaryTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createTemporaryTableIfNotExists(Table table) { + return context.createTemporaryTableIfNotExists(table); + } + + @Override + public CreateTableColumnStep createGlobalTemporaryTable(String table) { + return context.createGlobalTemporaryTable(table); + } + + @Override + public CreateTableColumnStep createGlobalTemporaryTable(Name table) { + return context.createGlobalTemporaryTable(table); + } + + @Override + public CreateTableColumnStep createGlobalTemporaryTable(Table table) { + return context.createGlobalTemporaryTable(table); + } + + @Override + public CreateViewAsStep createView(String view, String... fields) { + return context.createView(view, fields); + } + + @Override + public CreateViewAsStep createView(Name view, Name... fields) { + return context.createView(view, fields); + } + + @Override + public CreateViewAsStep createView(Table view, Field... fields) { + return context.createView(view, fields); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(String view, Function, ? extends String> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(String view, BiFunction, ? super Integer, ? extends String> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(Name view, Function, ? extends Name> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(Name view, BiFunction, ? super Integer, ? extends Name> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(Table view, Function, ? extends Field> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createView(Table view, BiFunction, ? super Integer, ? extends Field> fieldNameFunction) { + return context.createView(view, fieldNameFunction); + } + + @Override + public CreateViewAsStep createOrReplaceView(String view, String... fields) { + return context.createOrReplaceView(view, fields); + } + + @Override + public CreateViewAsStep createOrReplaceView(Name view, Name... fields) { + return context.createOrReplaceView(view, fields); + } + + @Override + public CreateViewAsStep createOrReplaceView(Table view, Field... fields) { + return context.createOrReplaceView(view, fields); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(String view, Function, ? extends String> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(String view, BiFunction, ? super Integer, ? extends String> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(Name view, Function, ? extends Name> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(Name view, BiFunction, ? super Integer, ? extends Name> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(Table view, Function, ? extends Field> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createOrReplaceView(Table view, BiFunction, ? super Integer, ? extends Field> fieldNameFunction) { + return context.createOrReplaceView(view, fieldNameFunction); + } + + @Override + public CreateViewAsStep createViewIfNotExists(String view, String... fields) { + return context.createViewIfNotExists(view, fields); + } + + @Override + public CreateViewAsStep createViewIfNotExists(Name view, Name... fields) { + return context.createViewIfNotExists(view, fields); + } + + @Override + public CreateViewAsStep createViewIfNotExists(Table view, Field... fields) { + return context.createViewIfNotExists(view, fields); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(String view, Function, ? extends String> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(String view, BiFunction, ? super Integer, ? extends String> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(Name view, Function, ? extends Name> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(Name view, BiFunction, ? super Integer, ? extends Name> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(Table view, Function, ? extends Field> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @SuppressWarnings("deprecation") + @Override + public CreateViewAsStep createViewIfNotExists(Table view, BiFunction, ? super Integer, ? extends Field> fieldNameFunction) { + return context.createViewIfNotExists(view, fieldNameFunction); + } + + @Override + public CreateTypeStep createType(String type) { + return context.createType(type); + } + + @Override + public CreateTypeStep createType(Name type) { + return context.createType(type); + } + + @Override + public AlterTypeStep alterType(String type) { + return context.alterType(type); + } + + @Override + public AlterTypeStep alterType(Name type) { + return context.alterType(type); + } + + @Override + public DropTypeStep dropType(String type) { + return context.dropType(type); + } + + @Override + public DropTypeStep dropType(Name type) { + return context.dropType(type); + } + + @Override + public DropTypeStep dropType(String... type) { + return context.dropType(type); + } + + @Override + public DropTypeStep dropType(Name... type) { + return context.dropType(type); + } + + @Override + public DropTypeStep dropType(Collection type) { + return context.dropType(type); + } + + @Override + public DropTypeStep dropTypeIfExists(String type) { + return context.dropTypeIfExists(type); + } + + @Override + public DropTypeStep dropTypeIfExists(Name type) { + return context.dropTypeIfExists(type); + } + + @Override + public DropTypeStep dropTypeIfExists(String... type) { + return context.dropTypeIfExists(type); + } + + @Override + public DropTypeStep dropTypeIfExists(Name... type) { + return context.dropTypeIfExists(type); + } + + @Override + public DropTypeStep dropTypeIfExists(Collection type) { + return context.dropTypeIfExists(type); + } + + @Override + public CreateIndexStep createIndex() { + return context.createIndex(); + } + + @Override + public CreateIndexStep createIndex(String index) { + return context.createIndex(index); + } + + @Override + public CreateIndexStep createIndex(Name index) { + return context.createIndex(index); + } + + @Override + public CreateIndexStep createIndex(Index index) { + return context.createIndex(index); + } + + @Override + public CreateIndexStep createIndexIfNotExists(String index) { + return context.createIndexIfNotExists(index); + } + + @Override + public CreateIndexStep createIndexIfNotExists(Name index) { + return context.createIndexIfNotExists(index); + } + + @Override + public CreateIndexStep createIndexIfNotExists(Index index) { + return context.createIndexIfNotExists(index); + } + + @Override + public CreateIndexStep createUniqueIndex() { + return context.createUniqueIndex(); + } + + @Override + public CreateIndexStep createUniqueIndex(String index) { + return context.createUniqueIndex(index); + } + + @Override + public CreateIndexStep createUniqueIndex(Name index) { + return context.createUniqueIndex(index); + } + + @Override + public CreateIndexStep createUniqueIndex(Index index) { + return context.createUniqueIndex(index); + } + + @Override + public CreateIndexStep createUniqueIndexIfNotExists(String index) { + return context.createUniqueIndexIfNotExists(index); + } + + @Override + public CreateIndexStep createUniqueIndexIfNotExists(Name index) { + return context.createUniqueIndexIfNotExists(index); + } + + @Override + public CreateIndexStep createUniqueIndexIfNotExists(Index index) { + return context.createUniqueIndexIfNotExists(index); + } + + @Override + public CreateSequenceFlagsStep createSequence(String sequence) { + return context.createSequence(sequence); + } + + @Override + public CreateSequenceFlagsStep createSequence(Name sequence) { + return context.createSequence(sequence); + } + + @Override + public CreateSequenceFlagsStep createSequence(Sequence sequence) { + return context.createSequence(sequence); + } + + @Override + public CreateSequenceFlagsStep createSequenceIfNotExists(String sequence) { + return context.createSequenceIfNotExists(sequence); + } + + @Override + public CreateSequenceFlagsStep createSequenceIfNotExists(Name sequence) { + return context.createSequenceIfNotExists(sequence); + } + + @Override + public CreateSequenceFlagsStep createSequenceIfNotExists(Sequence sequence) { + return context.createSequenceIfNotExists(sequence); + } + + @Override + public AlterDatabaseStep alterDatabase(String database) { + return context.alterDatabase(database); + } + + @Override + public AlterDatabaseStep alterDatabase(Name database) { + return context.alterDatabase(database); + } + + @Override + public AlterDatabaseStep alterDatabase(Catalog database) { + return context.alterDatabase(database); + } + + @Override + public AlterDatabaseStep alterDatabaseIfExists(String database) { + return context.alterDatabaseIfExists(database); + } + + @Override + public AlterDatabaseStep alterDatabaseIfExists(Name database) { + return context.alterDatabaseIfExists(database); + } + + @Override + public AlterDatabaseStep alterDatabaseIfExists(Catalog database) { + return context.alterDatabaseIfExists(database); + } + + @Override + public AlterDomainStep alterDomain(String domain) { + return context.alterDomain(domain); + } + + @Override + public AlterDomainStep alterDomain(Name domain) { + return context.alterDomain(domain); + } + + @Override + public AlterDomainStep alterDomain(Domain domain) { + return context.alterDomain(domain); + } + + @Override + public AlterDomainStep alterDomainIfExists(String domain) { + return context.alterDomainIfExists(domain); + } + + @Override + public AlterDomainStep alterDomainIfExists(Name domain) { + return context.alterDomainIfExists(domain); + } + + @Override + public AlterDomainStep alterDomainIfExists(Domain domain) { + return context.alterDomainIfExists(domain); + } + + @Override + public AlterSequenceStep alterSequence(String sequence) { + return context.alterSequence(sequence); + } + + @Override + public AlterSequenceStep alterSequence(Name sequence) { + return context.alterSequence(sequence); + } + + @Override + public AlterSequenceStep alterSequence(Sequence sequence) { + return context.alterSequence(sequence); + } + + @Override + public AlterSequenceStep alterSequenceIfExists(String sequence) { + return context.alterSequenceIfExists(sequence); + } + + @Override + public AlterSequenceStep alterSequenceIfExists(Name sequence) { + return context.alterSequenceIfExists(sequence); + } + + @Override + public AlterSequenceStep alterSequenceIfExists(Sequence sequence) { + return context.alterSequenceIfExists(sequence); + } + + @Override + public AlterTableStep alterTable(String table) { + return context.alterTable(table); + } + + @Override + public AlterTableStep alterTable(Name table) { + return context.alterTable(table); + } + + @Override + public AlterTableStep alterTable(Table table) { + return context.alterTable(table); + } + + @Override + public AlterTableStep alterTableIfExists(String table) { + return context.alterTableIfExists(table); + } + + @Override + public AlterTableStep alterTableIfExists(Name table) { + return context.alterTableIfExists(table); + } + + @Override + public AlterTableStep alterTableIfExists(Table table) { + return context.alterTableIfExists(table); + } + + @Override + public AlterSchemaStep alterSchema(String schema) { + return context.alterSchema(schema); + } + + @Override + public AlterSchemaStep alterSchema(Name schema) { + return context.alterSchema(schema); + } + + @Override + public AlterSchemaStep alterSchema(Schema schema) { + return context.alterSchema(schema); + } + + @Override + public AlterSchemaStep alterSchemaIfExists(String schema) { + return context.alterSchemaIfExists(schema); + } + + @Override + public AlterSchemaStep alterSchemaIfExists(Name schema) { + return context.alterSchemaIfExists(schema); + } + + @Override + public AlterSchemaStep alterSchemaIfExists(Schema schema) { + return context.alterSchemaIfExists(schema); + } + + @Override + public DropDatabaseFinalStep dropDatabase(String database) { + return context.dropDatabase(database); + } + + @Override + public DropDatabaseFinalStep dropDatabase(Name database) { + return context.dropDatabase(database); + } + + @Override + public DropDatabaseFinalStep dropDatabase(Catalog database) { + return context.dropDatabase(database); + } + + @Override + public DropDatabaseFinalStep dropDatabaseIfExists(String database) { + return context.dropDatabaseIfExists(database); + } + + @Override + public DropDatabaseFinalStep dropDatabaseIfExists(Name database) { + return context.dropDatabaseIfExists(database); + } + + @Override + public DropDatabaseFinalStep dropDatabaseIfExists(Catalog database) { + return context.dropDatabaseIfExists(database); + } + + @Override + public DropDomainCascadeStep dropDomain(String domain) { + return context.dropDomain(domain); + } + + @Override + public DropDomainCascadeStep dropDomain(Name domain) { + return context.dropDomain(domain); + } + + @Override + public DropDomainCascadeStep dropDomain(Domain domain) { + return context.dropDomain(domain); + } + + @Override + public DropDomainCascadeStep dropDomainIfExists(String domain) { + return context.dropDomainIfExists(domain); + } + + @Override + public DropDomainCascadeStep dropDomainIfExists(Name domain) { + return context.dropDomainIfExists(domain); + } + + @Override + public DropDomainCascadeStep dropDomainIfExists(Domain domain) { + return context.dropDomainIfExists(domain); + } + + @Override + public AlterViewStep alterView(String view) { + return context.alterView(view); + } + + @Override + public AlterViewStep alterView(Name view) { + return context.alterView(view); + } + + @Override + public AlterViewStep alterView(Table view) { + return context.alterView(view); + } + + @Override + public AlterViewStep alterViewIfExists(String view) { + return context.alterViewIfExists(view); + } + + @Override + public AlterViewStep alterViewIfExists(Name view) { + return context.alterViewIfExists(view); + } + + @Override + public AlterViewStep alterViewIfExists(Table view) { + return context.alterViewIfExists(view); + } + + @Override + public AlterIndexOnStep alterIndex(String index) { + return context.alterIndex(index); + } + + @Override + public AlterIndexOnStep alterIndex(Name index) { + return context.alterIndex(index); + } + + @Override + public AlterIndexOnStep alterIndex(Index index) { + return context.alterIndex(index); + } + + @Override + public AlterIndexStep alterIndexIfExists(String index) { + return context.alterIndexIfExists(index); + } + + @Override + public AlterIndexStep alterIndexIfExists(Name index) { + return context.alterIndexIfExists(index); + } + + @Override + public AlterIndexStep alterIndexIfExists(Index index) { + return context.alterIndexIfExists(index); + } + + @Override + public DropSchemaStep dropSchema(String schema) { + return context.dropSchema(schema); + } + + @Override + public DropSchemaStep dropSchema(Name schema) { + return context.dropSchema(schema); + } + + @Override + public DropSchemaStep dropSchema(Schema schema) { + return context.dropSchema(schema); + } + + @Override + public DropSchemaStep dropSchemaIfExists(String schema) { + return context.dropSchemaIfExists(schema); + } + + @Override + public DropSchemaStep dropSchemaIfExists(Name schema) { + return context.dropSchemaIfExists(schema); + } + + @Override + public DropSchemaStep dropSchemaIfExists(Schema schema) { + return context.dropSchemaIfExists(schema); + } + + @Override + public DropViewFinalStep dropView(String view) { + return context.dropView(view); + } + + @Override + public DropViewFinalStep dropView(Name view) { + return context.dropView(view); + } + + @Override + public DropViewFinalStep dropView(Table view) { + return context.dropView(view); + } + + @Override + public DropViewFinalStep dropViewIfExists(String view) { + return context.dropViewIfExists(view); + } + + @Override + public DropViewFinalStep dropViewIfExists(Name view) { + return context.dropViewIfExists(view); + } + + @Override + public DropViewFinalStep dropViewIfExists(Table view) { + return context.dropViewIfExists(view); + } + + @Override + public DropTableStep dropTable(String table) { + return context.dropTable(table); + } + + @Override + public DropTableStep dropTable(Name table) { + return context.dropTable(table); + } + + @Override + public DropTableStep dropTable(Table table) { + return context.dropTable(table); + } + + @Override + public DropTableStep dropTableIfExists(String table) { + return context.dropTableIfExists(table); + } + + @Override + public DropTableStep dropTableIfExists(Name table) { + return context.dropTableIfExists(table); + } + + @Override + public DropTableStep dropTableIfExists(Table table) { + return context.dropTableIfExists(table); + } + + @Override + public DropTableStep dropTemporaryTable(String table) { + return context.dropTemporaryTable(table); + } + + @Override + public DropTableStep dropTemporaryTable(Name table) { + return context.dropTemporaryTable(table); + } + + @Override + public DropTableStep dropTemporaryTable(Table table) { + return context.dropTemporaryTable(table); + } + + @Override + public DropTableStep dropTemporaryTableIfExists(String table) { + return context.dropTemporaryTableIfExists(table); + } + + @Override + public DropTableStep dropTemporaryTableIfExists(Name table) { + return context.dropTemporaryTableIfExists(table); + } + + @Override + public DropTableStep dropTemporaryTableIfExists(Table table) { + return context.dropTemporaryTableIfExists(table); + } + + @Override + public DropIndexOnStep dropIndex(String index) { + return context.dropIndex(index); + } + + @Override + public DropIndexOnStep dropIndex(Name index) { + return context.dropIndex(index); + } + + @Override + public DropIndexOnStep dropIndex(Index index) { + return context.dropIndex(index); + } + + @Override + public DropIndexOnStep dropIndexIfExists(String index) { + return context.dropIndexIfExists(index); + } + + @Override + public DropIndexOnStep dropIndexIfExists(Name index) { + return context.dropIndexIfExists(index); + } + + @Override + public DropIndexOnStep dropIndexIfExists(Index index) { + return context.dropIndexIfExists(index); + } + + @Override + public DropSequenceFinalStep dropSequence(String sequence) { + return context.dropSequence(sequence); + } + + @Override + public DropSequenceFinalStep dropSequence(Name sequence) { + return context.dropSequence(sequence); + } + + @Override + public DropSequenceFinalStep dropSequence(Sequence sequence) { + return context.dropSequence(sequence); + } + + @Override + public DropSequenceFinalStep dropSequenceIfExists(String sequence) { + return context.dropSequenceIfExists(sequence); + } + + @Override + public DropSequenceFinalStep dropSequenceIfExists(Name sequence) { + return context.dropSequenceIfExists(sequence); + } + + @Override + public DropSequenceFinalStep dropSequenceIfExists(Sequence sequence) { + return context.dropSequenceIfExists(sequence); + } + + @Override + public TruncateIdentityStep truncate(String table) { + return context.truncate(table); + } + + @Override + public TruncateIdentityStep truncate(Name table) { + return context.truncate(table); + } + + @Override + public TruncateIdentityStep truncate(Table table) { + return context.truncate(table); + } + + @Override + public TruncateIdentityStep truncateTable(String table) { + return context.truncateTable(table); + } + + @Override + public TruncateIdentityStep truncateTable(Name table) { + return context.truncateTable(table); + } + + @Override + public TruncateIdentityStep truncateTable(Table table) { + return context.truncateTable(table); + } + + @Override + public GrantOnStep grant(Privilege privilege) { + return context.grant(privilege); + } + + @Override + public GrantOnStep grant(Privilege... privileges) { + return context.grant(privileges); + } + + @Override + public GrantOnStep grant(Collection privileges) { + return context.grant(privileges); + } + + @Override + public RevokeOnStep revoke(Privilege privilege) { + return context.revoke(privilege); + } + + @Override + public RevokeOnStep revoke(Privilege... privileges) { + return context.revoke(privileges); + } + + @Override + public RevokeOnStep revoke(Collection privileges) { + return context.revoke(privileges); + } + + @Override + public RevokeOnStep revokeGrantOptionFor(Privilege privilege) { + return context.revokeGrantOptionFor(privilege); + } + + @Override + public RevokeOnStep revokeGrantOptionFor(Privilege... privileges) { + return context.revokeGrantOptionFor(privileges); + } + + @Override + public RevokeOnStep revokeGrantOptionFor(Collection privileges) { + return context.revokeGrantOptionFor(privileges); + } + + @Override + public BigInteger lastID() throws DataAccessException { + return context.lastID(); + } + + @Override + public BigInteger nextval(String sequence) throws DataAccessException { + return context.nextval(sequence); + } + + @Override + public BigInteger nextval(Name sequence) throws DataAccessException { + return context.nextval(sequence); + } + + @Override + public T nextval(Sequence sequence) throws DataAccessException { + return context.nextval(sequence); + } + + @Override + public List nextvals(Sequence sequence, int size) throws DataAccessException { + return context.nextvals(sequence, size); + } + + @Override + public BigInteger currval(String sequence) throws DataAccessException { + return context.currval(sequence); + } + + @Override + public BigInteger currval(Name sequence) throws DataAccessException { + return context.currval(sequence); + } + + @Override + public T currval(Sequence sequence) throws DataAccessException { + return context.currval(sequence); + } + + @Override + public > R newRecord(UDT type) { + return context.newRecord(type); + } + + @Override + public R newRecord(Table table) { + return context.newRecord(table); + } + + @Override + public R newRecord(Table table, Object source) { + return context.newRecord(table, source); + } + + @Override + public Record newRecord(Field... fields) { + return context.newRecord(fields); + } + + @Override + public Record newRecord(Collection> fields) { + return context.newRecord(fields); + } + + @Override + public Record1 newRecord(Field field1) { + return context.newRecord(field1); + } + + @Override + public Record2 newRecord(Field field1, Field field2) { + return context.newRecord(field1, field2); + } + + @Override + public Record3 newRecord(Field field1, Field field2, Field field3) { + return context.newRecord(field1, field2, field3); + } + + @Override + public Record4 newRecord(Field field1, Field field2, Field field3, Field field4) { + return context.newRecord(field1, field2, field3, field4); + } + + @Override + public Record5 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5) { + return context.newRecord(field1, field2, field3, field4, field5); + } + + @Override + public Record6 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6) { + return context.newRecord(field1, field2, field3, field4, field5, field6); + } + + @Override + public Record7 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7); + } + + @Override + public Record8 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8); + } + + @Override + public Record9 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9); + } + + @Override + public Record10 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10); + } + + @Override + public Record11 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11); + } + + @Override + public Record12 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12); + } + + @Override + public Record13 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13); + } + + @Override + public Record14 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14); + } + + @Override + public Record15 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15); + } + + @Override + public Record16 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16); + } + + @Override + public Record17 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17); + } + + @Override + public Record18 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18); + } + + @Override + public Record19 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19); + } + + @Override + public Record20 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20); + } + + @Override + public Record21 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21); + } + + @Override + public Record22 newRecord(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21, Field field22) { + return context.newRecord(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22); + } + + @Override + public Result newResult(Table table) { + return context.newResult(table); + } + + @Override + public Result newResult(Field... fields) { + return context.newResult(fields); + } + + @Override + public Result newResult(Collection> fields) { + return context.newResult(fields); + } + + @Override + public Result> newResult(Field field1) { + return context.newResult(field1); + } + + @Override + public Result> newResult(Field field1, Field field2) { + return context.newResult(field1, field2); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3) { + return context.newResult(field1, field2, field3); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4) { + return context.newResult(field1, field2, field3, field4); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5) { + return context.newResult(field1, field2, field3, field4, field5); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6) { + return context.newResult(field1, field2, field3, field4, field5, field6); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21); + } + + @Override + public Result> newResult(Field field1, Field field2, Field field3, Field field4, Field field5, Field field6, Field field7, Field field8, Field field9, Field field10, Field field11, Field field12, Field field13, Field field14, Field field15, Field field16, Field field17, Field field18, Field field19, Field field20, Field field21, Field field22) { + return context.newResult(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22); + } + + @Override + public Result fetch(ResultQuery query) throws DataAccessException { + return context.fetch(query); + } + + @Override + public Cursor fetchLazy(ResultQuery query) throws DataAccessException { + return context.fetchLazy(query); + } + + @Override + public CompletionStage> fetchAsync(ResultQuery query) { + return context.fetchAsync(query); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, ResultQuery query) { + return context.fetchAsync(executor, query); + } + + @Override + public Stream fetchStream(ResultQuery query) throws DataAccessException { + return context.fetchStream(query); + } + + @Override + public Results fetchMany(ResultQuery query) throws DataAccessException { + return context.fetchMany(query); + } + + @Override + public R fetchOne(ResultQuery query) throws DataAccessException, TooManyRowsException { + return context.fetchOne(query); + } + + @Override + public R fetchSingle(ResultQuery query) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(query); + } + + @Override + public Optional fetchOptional(ResultQuery query) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(query); + } + + @Override + public T fetchValue(Table> table) throws DataAccessException, TooManyRowsException { + return context.fetchValue(table); + } + + @Override + public > T fetchValue(ResultQuery query) throws DataAccessException, TooManyRowsException { + return context.fetchValue(query); + } + + @Override + public T fetchValue(TableField field) throws DataAccessException, TooManyRowsException { + return context.fetchValue(field); + } + + @Override + public T fetchValue(Field field) throws DataAccessException { + return context.fetchValue(field); + } + + @Override + public > Optional fetchOptionalValue(ResultQuery query) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(query); + } + + @Override + public Optional fetchOptionalValue(TableField field) throws DataAccessException, TooManyRowsException, InvalidResultException { + return context.fetchOptionalValue(field); + } + + @Override + public List fetchValues(Table> table) throws DataAccessException { + return context.fetchValues(table); + } + + @Override + public > List fetchValues(ResultQuery query) throws DataAccessException { + return context.fetchValues(query); + } + + @Override + public List fetchValues(TableField field) throws DataAccessException { + return context.fetchValues(field); + } + + @Override + public > Result fetchByExample(R example) throws DataAccessException { + return context.fetchByExample(example); + } + + @Override + public int fetchCount(Select query) throws DataAccessException { + return context.fetchCount(query); + } + + @Override + public int fetchCount(Table table) throws DataAccessException { + return context.fetchCount(table); + } + + @Override + public int fetchCount(Table table, Condition condition) throws DataAccessException { + return context.fetchCount(table, condition); + } + + @Override + public int fetchCount(Table table, Condition... conditions) throws DataAccessException { + return context.fetchCount(table, conditions); + } + + @Override + public int fetchCount(Table table, Collection conditions) throws DataAccessException { + return context.fetchCount(table, conditions); + } + + @Override + public boolean fetchExists(Select query) throws DataAccessException { + return context.fetchExists(query); + } + + @Override + public boolean fetchExists(Table table) throws DataAccessException { + return context.fetchExists(table); + } + + @Override + public boolean fetchExists(Table table, Condition condition) throws DataAccessException { + return context.fetchExists(table, condition); + } + + @Override + public boolean fetchExists(Table table, Condition... conditions) throws DataAccessException { + return context.fetchExists(table, conditions); + } + + @Override + public boolean fetchExists(Table table, Collection conditions) throws DataAccessException { + return context.fetchExists(table, conditions); + } + + @Override + public int execute(Query query) throws DataAccessException { + return context.execute(query); + } + + @Override + public Result fetch(Table table) throws DataAccessException { + return context.fetch(table); + } + + @Override + public Result fetch(Table table, Condition condition) throws DataAccessException { + return context.fetch(table, condition); + } + + @Override + public Result fetch(Table table, Condition... conditions) throws DataAccessException { + return context.fetch(table, conditions); + } + + @Override + public Result fetch(Table table, Collection conditions) throws DataAccessException { + return context.fetch(table, conditions); + } + + @Override + public R fetchOne(Table table) throws DataAccessException, TooManyRowsException { + return context.fetchOne(table); + } + + @Override + public R fetchOne(Table table, Condition condition) throws DataAccessException, TooManyRowsException { + return context.fetchOne(table, condition); + } + + @Override + public R fetchOne(Table table, Condition... conditions) throws DataAccessException, TooManyRowsException { + return context.fetchOne(table, conditions); + } + + @Override + public R fetchOne(Table table, Collection conditions) throws DataAccessException, TooManyRowsException { + return context.fetchOne(table, conditions); + } + + @Override + public R fetchSingle(Table table) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(table); + } + + @Override + public R fetchSingle(Table table, Condition condition) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(table, condition); + } + + @Override + public R fetchSingle(Table table, Condition... conditions) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(table, conditions); + } + + @Override + public R fetchSingle(Table table, Collection conditions) throws DataAccessException, NoDataFoundException, TooManyRowsException { + return context.fetchSingle(table, conditions); + } + + @Override + public Record fetchSingle(SelectField... fields) throws DataAccessException { + return context.fetchSingle(fields); + } + + @Override + public Record fetchSingle(Collection> fields) throws DataAccessException { + return context.fetchSingle(fields); + } + + @Override + public Record1 fetchSingle(SelectField field1) throws DataAccessException { + return context.fetchSingle(field1); + } + + @Override + public Record2 fetchSingle(SelectField field1, SelectField field2) throws DataAccessException { + return context.fetchSingle(field1, field2); + } + + @Override + public Record3 fetchSingle(SelectField field1, SelectField field2, SelectField field3) throws DataAccessException { + return context.fetchSingle(field1, field2, field3); + } + + @Override + public Record4 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4); + } + + @Override + public Record5 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5); + } + + @Override + public Record6 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6); + } + + @Override + public Record7 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7); + } + + @Override + public Record8 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8); + } + + @Override + public Record9 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9); + } + + @Override + public Record10 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10); + } + + @Override + public Record11 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11); + } + + @Override + public Record12 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12); + } + + @Override + public Record13 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13); + } + + @Override + public Record14 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14); + } + + @Override + public Record15 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15); + } + + @Override + public Record16 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16); + } + + @Override + public Record17 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17); + } + + @Override + public Record18 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18); + } + + @Override + public Record19 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19); + } + + @Override + public Record20 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20); + } + + @Override + public Record21 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21); + } + + @Override + public Record22 fetchSingle(SelectField field1, SelectField field2, SelectField field3, SelectField field4, SelectField field5, SelectField field6, SelectField field7, SelectField field8, SelectField field9, SelectField field10, SelectField field11, SelectField field12, SelectField field13, SelectField field14, SelectField field15, SelectField field16, SelectField field17, SelectField field18, SelectField field19, SelectField field20, SelectField field21, SelectField field22) throws DataAccessException { + return context.fetchSingle(field1, field2, field3, field4, field5, field6, field7, field8, field9, field10, field11, field12, field13, field14, field15, field16, field17, field18, field19, field20, field21, field22); + } + + @Override + public Optional fetchOptional(Table table) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(table); + } + + @Override + public Optional fetchOptional(Table table, Condition condition) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(table, condition); + } + + @Override + public Optional fetchOptional(Table table, Condition... conditions) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(table, conditions); + } + + @Override + public Optional fetchOptional(Table table, Collection conditions) throws DataAccessException, TooManyRowsException { + return context.fetchOptional(table, conditions); + } + + @Override + public R fetchAny(Table table) throws DataAccessException { + return context.fetchAny(table); + } + + @Override + public R fetchAny(Table table, Condition condition) throws DataAccessException { + return context.fetchAny(table, condition); + } + + @Override + public R fetchAny(Table table, Condition... conditions) throws DataAccessException { + return context.fetchAny(table, conditions); + } + + @Override + public R fetchAny(Table table, Collection conditions) throws DataAccessException { + return context.fetchAny(table, conditions); + } + + @Override + public Cursor fetchLazy(Table table) throws DataAccessException { + return context.fetchLazy(table); + } + + @Override + public Cursor fetchLazy(Table table, Condition condition) throws DataAccessException { + return context.fetchLazy(table, condition); + } + + @Override + public Cursor fetchLazy(Table table, Condition... conditions) throws DataAccessException { + return context.fetchLazy(table, conditions); + } + + @Override + public Cursor fetchLazy(Table table, Collection conditions) throws DataAccessException { + return context.fetchLazy(table, conditions); + } + + @Override + public CompletionStage> fetchAsync(Table table) { + return context.fetchAsync(table); + } + + @Override + public CompletionStage> fetchAsync(Table table, Condition condition) { + return context.fetchAsync(table, condition); + } + + @Override + public CompletionStage> fetchAsync(Table table, Condition... condition) { + return context.fetchAsync(table, condition); + } + + @Override + public CompletionStage> fetchAsync(Table table, Collection condition) { + return context.fetchAsync(table, condition); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, Table table) { + return context.fetchAsync(executor, table); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, Table table, Condition condition) { + return context.fetchAsync(executor, table, condition); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, Table table, Condition... conditions) { + return context.fetchAsync(executor, table, conditions); + } + + @Override + public CompletionStage> fetchAsync(Executor executor, Table table, Collection conditions) { + return context.fetchAsync(executor, table, conditions); + } + + @Override + public Stream fetchStream(Table table) throws DataAccessException { + return context.fetchStream(table); + } + + @Override + public Stream fetchStream(Table table, Condition condition) throws DataAccessException { + return context.fetchStream(table, condition); + } + + @Override + public Stream fetchStream(Table table, Condition... conditions) throws DataAccessException { + return context.fetchStream(table, conditions); + } + + @Override + public Stream fetchStream(Table table, Collection conditions) throws DataAccessException { + return context.fetchStream(table, conditions); + } + + @Override + public int executeInsert(TableRecord record) throws DataAccessException { + return context.executeInsert(record); + } + + @Override + public int executeUpdate(UpdatableRecord record) throws DataAccessException { + return context.executeUpdate(record); + } + + @Override + public int executeUpdate(TableRecord record, Condition condition) throws DataAccessException { + return context.executeUpdate(record, condition); + } + + @Override + public int executeDelete(UpdatableRecord record) throws DataAccessException { + return context.executeDelete(record); + } + + @Override + public int executeDelete(TableRecord record, Condition condition) throws DataAccessException { + return context.executeDelete(record, condition); + } + + @Override + public Configuration configuration() { + return context.configuration(); + } + + @Override + public DSLContext dsl() { + return context.dsl(); + } + + @Override + public Settings settings() { + return context.settings(); + } + + @Override + public SQLDialect dialect() { + return context.dialect(); + } + + @Override + public SQLDialect family() { + return context.family(); + } + + @Override + public Map data() { + return context.data(); + } + + @Override + public Object data(Object key) { + return context.data(key); + } + + @Override + public Object data(Object key, Object value) { + return context.data(key, value); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/db/PostgreSQLDatabaseMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/db/PostgreSQLDatabaseMetrics.java new file mode 100644 index 0000000000..ca9d0d4195 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/db/PostgreSQLDatabaseMetrics.java @@ -0,0 +1,339 @@ +/* + * Copyright 2017 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.binder.db; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.DoubleSupplier; + +import javax.sql.DataSource; + +import io.micrometer.core.instrument.FunctionCounter; +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * {@link MeterBinder} for a PostgreSQL database. + * + * @author Kristof Depypere + * @author Jon Schneider + * @author Johnny Lim + * @author Markus Dobel + * @since 1.1.0 + */ +@NonNullApi +@NonNullFields +public class PostgreSQLDatabaseMetrics implements MeterBinder { + + private static final String SELECT = "SELECT "; + + private static final String QUERY_DEAD_TUPLE_COUNT = getUserTableQuery("SUM(n_dead_tup)"); + private static final String QUERY_TIMED_CHECKPOINTS_COUNT = getBgWriterQuery("checkpoints_timed"); + private static final String QUERY_REQUESTED_CHECKPOINTS_COUNT = getBgWriterQuery("checkpoints_req"); + private static final String QUERY_BUFFERS_CLEAN = getBgWriterQuery("buffers_clean"); + private static final String QUERY_BUFFERS_BACKEND = getBgWriterQuery("buffers_backend"); + private static final String QUERY_BUFFERS_CHECKPOINT = getBgWriterQuery("buffers_checkpoint"); + + private final String database; + private final DataSource postgresDataSource; + private final Iterable tags; + private final Map beforeResetValuesCacheMap; + private final Map previousValueCacheMap; + + private final String queryConnectionCount; + private final String queryReadCount; + private final String queryInsertCount; + private final String queryTempBytes; + private final String queryUpdateCount; + private final String queryDeleteCount; + private final String queryBlockHits; + private final String queryBlockReads; + private final String queryTransactionCount; + + public PostgreSQLDatabaseMetrics(DataSource postgresDataSource, String database) { + this(postgresDataSource, database, Tags.empty()); + } + + public PostgreSQLDatabaseMetrics(DataSource postgresDataSource, String database, Iterable tags) { + this.postgresDataSource = postgresDataSource; + this.database = database; + this.tags = Tags.of(tags).and(createDbTag(database)); + this.beforeResetValuesCacheMap = new ConcurrentHashMap<>(); + this.previousValueCacheMap = new ConcurrentHashMap<>(); + + this.queryConnectionCount = getDBStatQuery(database, "numbackends"); + this.queryReadCount = getDBStatQuery(database, "tup_fetched"); + this.queryInsertCount = getDBStatQuery(database, "tup_inserted"); + this.queryTempBytes = getDBStatQuery(database, "temp_bytes"); + this.queryUpdateCount = getDBStatQuery(database, "tup_updated"); + this.queryDeleteCount = getDBStatQuery(database, "tup_deleted"); + this.queryBlockHits = getDBStatQuery(database, "blks_hit"); + this.queryBlockReads = getDBStatQuery(database, "blks_read"); + this.queryTransactionCount = getDBStatQuery(database, "xact_commit + xact_rollback"); + } + + private static Tag createDbTag(String database) { + return Tag.of("database", database); + } + + @Override + public void bindTo(MeterRegistry registry) { + Gauge.builder(Names.SIZE, postgresDataSource, dataSource -> getDatabaseSize()) + .tags(tags) + .description("The database size") + .register(registry); + Gauge.builder(Names.CONNECTIONS, postgresDataSource, dataSource -> getConnectionCount()) + .tags(tags) + .description("Number of active connections to the given db") + .register(registry); + + // Hit ratio can be derived from dividing hits/reads + FunctionCounter.builder(Names.BLOCKS_HITS, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.BLOCKS_HITS, this::getBlockHits)) + .tags(tags) + .description("Number of times disk blocks were found already in the buffer cache, so that a read was not necessary") + .register(registry); + FunctionCounter.builder(Names.BLOCKS_READS, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.BLOCKS_READS, this::getBlockReads)) + .tags(tags) + .description("Number of disk blocks read in this database") + .register(registry); + + FunctionCounter.builder(Names.TRANSACTIONS, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.TRANSACTIONS, this::getTransactionCount)) + .tags(tags) + .description("Total number of transactions executed (commits + rollbacks)") + .register(registry); + Gauge.builder(Names.LOCKS, postgresDataSource, dataSource -> getLockCount()) + .tags(tags) + .description("Number of locks on the given db") + .register(registry); + FunctionCounter.builder(Names.TEMP_WRITES, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.TEMP_WRITES, this::getTempBytes)) + .tags(tags) + .description("The total amount of temporary writes to disk to execute queries") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + registerRowCountMetrics(registry); + registerCheckpointMetrics(registry); + } + + private void registerRowCountMetrics(MeterRegistry registry) { + FunctionCounter.builder(Names.ROWS_FETCHED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.ROWS_FETCHED, this::getReadCount)) + .tags(tags) + .description("Number of rows fetched from the db") + .register(registry); + FunctionCounter.builder(Names.ROWS_INSERTED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.ROWS_INSERTED, this::getInsertCount)) + .tags(tags) + .description("Number of rows inserted from the db") + .register(registry); + FunctionCounter.builder(Names.ROWS_UPDATED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.ROWS_UPDATED, this::getUpdateCount)) + .tags(tags) + .description("Number of rows updated from the db") + .register(registry); + FunctionCounter.builder(Names.ROWS_DELETED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.ROWS_DELETED, this::getDeleteCount)) + .tags(tags) + .description("Number of rows deleted from the db") + .register(registry); + Gauge.builder(Names.ROWS_DEAD, postgresDataSource, dataSource -> getDeadTupleCount()) + .tags(tags) + .description("Total number of dead rows in the current database") + .register(registry); + } + + private void registerCheckpointMetrics(MeterRegistry registry) { + FunctionCounter.builder(Names.CHECKPOINTS_TIMED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.CHECKPOINTS_TIMED, this::getTimedCheckpointsCount)) + .tags(tags) + .description("Number of checkpoints timed") + .register(registry); + FunctionCounter.builder(Names.CHECKPOINTS_REQUESTED, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.CHECKPOINTS_REQUESTED, this::getRequestedCheckpointsCount)) + .tags(tags) + .description("Number of checkpoints requested") + .register(registry); + + FunctionCounter.builder(Names.BUFFERS_CHECKPOINT, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.BUFFERS_CHECKPOINT, this::getBuffersCheckpoint)) + .tags(tags) + .description("Number of buffers written during checkpoints") + .register(registry); + FunctionCounter.builder(Names.BUFFERS_CLEAN, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.BUFFERS_CLEAN, this::getBuffersClean)) + .tags(tags) + .description("Number of buffers written by the background writer") + .register(registry); + FunctionCounter.builder(Names.BUFFERS_BACKEND, postgresDataSource, + dataSource -> resettableFunctionalCounter(Names.BUFFERS_BACKEND, this::getBuffersBackend)) + .tags(tags) + .description("Number of buffers written directly by a backend") + .register(registry); + } + + private Long getDatabaseSize() { + return runQuery("SELECT pg_database_size('" + database + "')"); + } + + private Long getLockCount() { + return runQuery("SELECT count(*) FROM pg_locks l JOIN pg_database d ON l.DATABASE=d.oid WHERE d.datname='" + database + "'"); + } + + private Long getConnectionCount() { + return runQuery(this.queryConnectionCount); + } + + private Long getReadCount() { + return runQuery(this.queryReadCount); + } + + private Long getInsertCount() { + return runQuery(this.queryInsertCount); + } + + private Long getTempBytes() { + return runQuery(this.queryTempBytes); + } + + private Long getUpdateCount() { + return runQuery(this.queryUpdateCount); + } + + private Long getDeleteCount() { + return runQuery(this.queryDeleteCount); + } + + private Long getBlockHits() { + return runQuery(this.queryBlockHits); + } + + private Long getBlockReads() { + return runQuery(this.queryBlockReads); + } + + private Long getTransactionCount() { + return runQuery(this.queryTransactionCount); + } + + private Long getDeadTupleCount() { + return runQuery(QUERY_DEAD_TUPLE_COUNT); + } + + private Long getTimedCheckpointsCount() { + return runQuery(QUERY_TIMED_CHECKPOINTS_COUNT); + } + + private Long getRequestedCheckpointsCount() { + return runQuery(QUERY_REQUESTED_CHECKPOINTS_COUNT); + } + + private Long getBuffersClean() { + return runQuery(QUERY_BUFFERS_CLEAN); + } + + private Long getBuffersBackend() { + return runQuery(QUERY_BUFFERS_BACKEND); + } + + private Long getBuffersCheckpoint() { + return runQuery(QUERY_BUFFERS_CHECKPOINT); + } + + /** + * Function that makes sure functional counter values survive pg_stat_reset calls. + */ + Double resettableFunctionalCounter(String functionalCounterKey, DoubleSupplier function) { + Double result = function.getAsDouble(); + Double previousResult = previousValueCacheMap.getOrDefault(functionalCounterKey, 0D); + Double beforeResetValue = beforeResetValuesCacheMap.getOrDefault(functionalCounterKey, 0D); + Double correctedValue = result + beforeResetValue; + + if (correctedValue < previousResult) { + beforeResetValuesCacheMap.put(functionalCounterKey, previousResult); + correctedValue = previousResult + result; + } + previousValueCacheMap.put(functionalCounterKey, correctedValue); + return correctedValue; + } + + private Long runQuery(String query) { + try (Connection connection = postgresDataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(query)) { + if (resultSet.next()) { + return resultSet.getLong(1); + } + } catch (SQLException ignored) { + } + return 0L; + } + + private static String getDBStatQuery(String database, String statName) { + return SELECT + statName + " FROM pg_stat_database WHERE datname = '" + database + "'"; + } + + private static String getUserTableQuery(String statName) { + return SELECT + statName + " FROM pg_stat_user_tables"; + } + + private static String getBgWriterQuery(String statName) { + return SELECT + statName + " FROM pg_stat_bgwriter"; + } + + static final class Names { + static final String SIZE = of("size"); + static final String CONNECTIONS = of("connections"); + static final String BLOCKS_HITS = of("blocks.hits"); + static final String BLOCKS_READS = of("blocks.reads"); + static final String TRANSACTIONS = of("transactions"); + static final String LOCKS = of("locks"); + static final String TEMP_WRITES = of("temp.writes"); + + static final String ROWS_FETCHED = of("rows.fetched"); + static final String ROWS_INSERTED = of("rows.inserted"); + static final String ROWS_UPDATED = of("rows.updated"); + static final String ROWS_DELETED = of("rows.deleted"); + static final String ROWS_DEAD = of("rows.dead"); + + static final String CHECKPOINTS_TIMED = of("checkpoints.timed"); + static final String CHECKPOINTS_REQUESTED = of("checkpoints.requested"); + + static final String BUFFERS_CHECKPOINT = of("buffers.checkpoint"); + static final String BUFFERS_CLEAN = of("buffers.clean"); + static final String BUFFERS_BACKEND = of("buffers.backend"); + + private static String of(String name) { + return "postgres." + name; + } + + private Names() { + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/AbstractMetricCollectingInterceptor.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/AbstractMetricCollectingInterceptor.java new file mode 100644 index 0000000000..3136fac9df --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/AbstractMetricCollectingInterceptor.java @@ -0,0 +1,278 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import io.grpc.MethodDescriptor; +import io.grpc.ServiceDescriptor; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Sample; +import io.micrometer.core.instrument.binder.BaseUnits; + +/** + * An abstract gRPC interceptor that will collect metrics. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 1.7.0 + */ +public abstract class AbstractMetricCollectingInterceptor { + + /** + * The metrics tag key that belongs to the called service name. + */ + private static final String TAG_SERVICE_NAME = "service"; + /** + * The metrics tag key that belongs to the called method name. + */ + private static final String TAG_METHOD_NAME = "method"; + /** + * The metrics tag key that belongs to the type of the called method. + */ + private static final String TAG_METHOD_TYPE = "methodType"; + /** + * The metrics tag key that belongs to the result status code. + */ + private static final String TAG_STATUS_CODE = "statusCode"; + + /** + * Creates a new counter builder for the given method. By default the base unit will be messages. + * + * @param method The method the counter will be created for. + * @param name The name of the counter to use. + * @param description The description of the counter to use. + * @return The newly created counter builder. + */ + protected static Counter.Builder prepareCounterFor(final MethodDescriptor method, + final String name, final String description) { + return Counter.builder(name) + .description(description) + .baseUnit(BaseUnits.MESSAGES) + .tag(TAG_SERVICE_NAME, method.getServiceName()) + .tag(TAG_METHOD_NAME, method.getBareMethodName()) + .tag(TAG_METHOD_TYPE, method.getType().name()); + } + + /** + * Creates a new timer builder for the given method. + * + * @param method The method the timer will be created for. + * @param name The name of the timer to use. + * @param description The description of the timer to use. + * @return The newly created timer builder. + */ + protected static Timer.Builder prepareTimerFor(final MethodDescriptor method, + final String name, final String description) { + return Timer.builder(name) + .description(description) + .tag(TAG_SERVICE_NAME, method.getServiceName()) + .tag(TAG_METHOD_NAME, method.getBareMethodName()) + .tag(TAG_METHOD_TYPE, method.getType().name()); + } + + private final Map, MetricSet> metricsForMethods = new ConcurrentHashMap<>(); + + protected final MeterRegistry registry; + + protected final UnaryOperator counterCustomizer; + protected final UnaryOperator timerCustomizer; + protected final Code[] eagerInitializedCodes; + + /** + * Creates a new gRPC interceptor that will collect metrics into the given {@link MeterRegistry}. This method won't + * use any customizers and will only initialize the {@link Code#OK OK} status. + * + * @param registry The registry to use. + */ + protected AbstractMetricCollectingInterceptor(final MeterRegistry registry) { + this(registry, UnaryOperator.identity(), UnaryOperator.identity(), Code.OK); + } + + /** + * Creates a new gRPC interceptor that will collect metrics into the given {@link MeterRegistry} and uses the given + * customizers to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + protected AbstractMetricCollectingInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + this.registry = registry; + this.counterCustomizer = counterCustomizer; + this.timerCustomizer = timerCustomizer; + this.eagerInitializedCodes = eagerInitializedCodes; + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param service The service to initialize the meters for. + * @see #preregisterMethod(MethodDescriptor) + */ + public void preregisterService(final ServiceDescriptor service) { + for (final MethodDescriptor method : service.getMethods()) { + preregisterMethod(method); + } + } + + /** + * Pre-registers the given method. This will initialize all default counters and timers for that method. + * + * @param method The method to initialize the meters for. + */ + public void preregisterMethod(final MethodDescriptor method) { + metricsFor(method); + } + + /** + * Gets or creates a {@link MetricSet} for the given gRPC method. This will initialize all default counters and + * timers for that method. + * + * @param method The method to get the metric set for. + * @return The metric set for the given method. + * @see #newMetricsFor(MethodDescriptor) + */ + protected final MetricSet metricsFor(final MethodDescriptor method) { + return this.metricsForMethods.computeIfAbsent(method, this::newMetricsFor); + } + + /** + * Creates a {@link MetricSet} for the given gRPC method. This will initialize all default counters and timers for + * that method. + * + * @param method The method to get the metric set for. + * @return The newly created metric set for the given method. + */ + protected MetricSet newMetricsFor(final MethodDescriptor method) { + return new MetricSet(newRequestCounterFor(method), newResponseCounterFor(method), newTimerFunction(method)); + } + + /** + * Creates a new request counter for the given method. + * + * @param method The method to create the counter for. + * @return The newly created request counter. + */ + protected abstract Counter newRequestCounterFor(final MethodDescriptor method); + + /** + * Creates a new response counter for the given method. + * + * @param method The method to create the counter for. + * @return The newly created response counter. + */ + protected abstract Counter newResponseCounterFor(final MethodDescriptor method); + + /** + * Creates a new timer function using the given template. This method initializes the default timers. + * + * @param timerTemplate The template to create the instances from. + * @return The newly created function that returns a timer for a given code. + */ + protected Function asTimerFunction(final Supplier timerTemplate) { + final Map cache = new EnumMap<>(Code.class); + final Function creator = code -> timerTemplate.get() + .tag(TAG_STATUS_CODE, code.name()) + .register(this.registry); + final Function cacheResolver = code -> cache.computeIfAbsent(code, creator); + // Eager initialize + for (final Code code : this.eagerInitializedCodes) { + cacheResolver.apply(code); + } + return cacheResolver; + } + + /** + * Creates a new function that returns a timer for a given code for the given method. + * + * @param method The method to create the timer for. + * @return The newly created function that returns a timer for a given code. + */ + protected abstract Function newTimerFunction(final MethodDescriptor method); + + /** + * Container for all metrics of a certain call. Used instead of 3 maps to improve performance. + */ + protected static class MetricSet { + + private final Counter requestCounter; + private final Counter responseCounter; + private final Function timerFunction; + + /** + * Creates a new metric set with the given meter instances. + * + * @param requestCounter The request counter to use. + * @param responseCounter The response counter to use. + * @param timerFunction The timer function to use. + */ + public MetricSet( + final Counter requestCounter, + final Counter responseCounter, + final Function timerFunction) { + + this.requestCounter = requestCounter; + this.responseCounter = responseCounter; + this.timerFunction = timerFunction; + } + + /** + * Gets the Counter that counts the request messages. + * + * @return The Counter that counts the request messages. + */ + public Counter getRequestCounter() { + return this.requestCounter; + } + + /** + * Gets the Counter that counts the response messages. + * + * @return The Counter that counts the response messages. + */ + public Counter getResponseCounter() { + return this.responseCounter; + } + + /** + * Uses the given registry to create a {@link Sample Timer.Sample} that will be reported if the returned + * consumer is invoked. + * + * @param registry The registry used to create the sample. + * @return The newly created consumer that will report the processing duration since calling this method and + * invoking the returned consumer along with the status code. + */ + public Consumer newProcessingDurationTiming(final MeterRegistry registry) { + final Sample timerSample = Timer.start(registry); + return code -> timerSample.stop(this.timerFunction.apply(code)); + } + + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCall.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCall.java new file mode 100644 index 0000000000..1a940b85e8 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCall.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.function.Consumer; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding client call that collects metrics. + * + * @param The type of message sent one or more times to the server. + * @param The type of message received one or more times from the server. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingClientCall extends SimpleForwardingClientCall { + + private final Counter requestCounter; + private final Counter responseCounter; + private final Consumer processingDurationTiming; + + /** + * Creates a new delegating ClientCall that will wrap the given client call to collect metrics. + * + * @param delegate The original call to wrap. + * @param requestCounter The counter for outgoing requests. + * @param responseCounter The counter for incoming responses. + * @param processingDurationTiming The consumer used to time the processing duration along with a response status. + */ + MetricCollectingClientCall( + final ClientCall delegate, + final Counter requestCounter, + final Counter responseCounter, + final Consumer processingDurationTiming) { + + super(delegate); + this.requestCounter = requestCounter; + this.responseCounter = responseCounter; + this.processingDurationTiming = processingDurationTiming; + } + + @Override + public void start(final Listener responseListener, final Metadata metadata) { + super.start( + new MetricCollectingClientCallListener<>( + responseListener, + this.responseCounter, + this.processingDurationTiming), + metadata); + } + + @Override + public void sendMessage(final Q requestMessage) { + this.requestCounter.increment(); + super.sendMessage(requestMessage); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCallListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCallListener.java new file mode 100644 index 0000000000..b2706cf3d5 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientCallListener.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.function.Consumer; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding client call listener that collects metrics. + * + * @param The type of message received one or more times from the server. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingClientCallListener extends SimpleForwardingClientCallListener { + + private final Counter responseCounter; + private final Consumer processingDurationTiming; + + /** + * Creates a new delegating {@link ClientCall.Listener} that will wrap the given client call listener to collect metrics. + * + * @param delegate The original call to wrap. + * @param responseCounter The counter for incoming responses. + * @param processingDurationTiming The consumer used to time the processing duration along with a response status. + */ + public MetricCollectingClientCallListener( + final ClientCall.Listener delegate, + final Counter responseCounter, + final Consumer processingDurationTiming) { + + super(delegate); + this.responseCounter = responseCounter; + this.processingDurationTiming = processingDurationTiming; + } + + @Override + public void onClose(final Status status, final Metadata metadata) { + this.processingDurationTiming.accept(status.getCode()); + super.onClose(status, metadata); + } + + @Override + public void onMessage(final A responseMessage) { + this.responseCounter.increment(); + super.onMessage(responseMessage); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientInterceptor.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientInterceptor.java new file mode 100644 index 0000000000..0f69fc1ac0 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingClientInterceptor.java @@ -0,0 +1,132 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +/** + * A gRPC client interceptor that will collect metrics using the given {@link MeterRegistry}. + * + *

+ * Usage: + *

+ * + *
+ * ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
+ *     .intercept(new MetricCollectingClientInterceptor(meterRegistry))
+ *     .build();
+ *
+ * channel.newCall(method, options);
+ * 
+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 1.7.0 + */ +public class MetricCollectingClientInterceptor extends AbstractMetricCollectingInterceptor + implements ClientInterceptor { + + /** + * The total number of requests sent + */ + private static final String METRIC_NAME_CLIENT_REQUESTS_SENT = "grpc.client.requests.sent"; + /** + * The total number of responses received + */ + private static final String METRIC_NAME_CLIENT_RESPONSES_RECEIVED = "grpc.client.responses.received"; + /** + * The total time taken for the client to complete the call, including network delay + */ + private static final String METRIC_NAME_CLIENT_PROCESSING_DURATION = "grpc.client.processing.duration"; + + /** + * Creates a new gRPC client interceptor that will collect metrics into the given {@link MeterRegistry}. + * + * @param registry The registry to use. + */ + public MetricCollectingClientInterceptor(final MeterRegistry registry) { + super(registry); + } + + /** + * Creates a new gRPC client interceptor that will collect metrics into the given {@link MeterRegistry} and uses the + * given customizers to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + public MetricCollectingClientInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + super(registry, counterCustomizer, timerCustomizer, eagerInitializedCodes); + } + + @Override + protected Counter newRequestCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_CLIENT_REQUESTS_SENT, + "The total number of requests sent")) + .register(this.registry); + } + + @Override + protected Counter newResponseCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_CLIENT_RESPONSES_RECEIVED, + "The total number of responses received")) + .register(this.registry); + } + + @Override + protected Function newTimerFunction(final MethodDescriptor method) { + return asTimerFunction(() -> this.timerCustomizer.apply( + prepareTimerFor(method, + METRIC_NAME_CLIENT_PROCESSING_DURATION, + "The total time taken for the client to complete the call, including network delay"))); + } + + @Override + public ClientCall interceptCall( + final MethodDescriptor methodDescriptor, + final CallOptions callOptions, + final Channel channel) { + + final MetricSet metrics = metricsFor(methodDescriptor); + final Consumer processingDurationTiming = metrics.newProcessingDurationTiming(this.registry); + + return new MetricCollectingClientCall<>( + channel.newCall(methodDescriptor, callOptions), + metrics.getRequestCounter(), + metrics.getResponseCounter(), + processingDurationTiming); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCall.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCall.java new file mode 100644 index 0000000000..a1f8eefa56 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCall.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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.binder.grpc; + +import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding server call that collects metrics. + * + * @param The type of message received one or more times from the client. + * @param
The type of message sent one or more times to the client. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingServerCall extends SimpleForwardingServerCall { + + private final Counter responseCounter; + private Code responseCode = Code.UNKNOWN; + + /** + * Creates a new delegating ServerCall that will wrap the given server call to collect metrics. + * + * @param delegate The original call to wrap. + * @param responseCounter The counter for outgoing responses. + */ + public MetricCollectingServerCall( + final ServerCall delegate, + final Counter responseCounter) { + + super(delegate); + this.responseCounter = responseCounter; + } + + public Code getResponseCode() { + return this.responseCode; + } + + @Override + public void close(final Status status, final Metadata responseHeaders) { + this.responseCode = status.getCode(); + super.close(status, responseHeaders); + } + + @Override + public void sendMessage(final A responseMessage) { + this.responseCounter.increment(); + super.sendMessage(responseMessage); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCallListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCallListener.java new file mode 100644 index 0000000000..c5a945cfd8 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerCallListener.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.ServerCall.Listener; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding server call listener that collects metrics. + * + * @param The type of message received one or more times from the client. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingServerCallListener extends SimpleForwardingServerCallListener { + + private final Counter requestCounter; + private final Supplier responseCodeSupplier; + private final Consumer responseStatusTiming; + + /** + * Creates a new delegating {@link Listener} that will wrap the given server call listener to collect metrics. + * + * @param delegate The original listener to wrap. + * @param requestCounter The counter for incoming requests. + * @param responseCodeSupplier The supplier of the response code. + * @param responseStatusTiming The consumer used to time the processing duration along with a response status. + */ + + public MetricCollectingServerCallListener( + final Listener delegate, + final Counter requestCounter, + final Supplier responseCodeSupplier, + final Consumer responseStatusTiming) { + + super(delegate); + this.requestCounter = requestCounter; + this.responseCodeSupplier = responseCodeSupplier; + this.responseStatusTiming = responseStatusTiming; + } + + @Override + public void onMessage(final Q requestMessage) { + this.requestCounter.increment(); + super.onMessage(requestMessage); + } + + @Override + public void onComplete() { + report(this.responseCodeSupplier.get()); + super.onComplete(); + } + + @Override + public void onCancel() { + report(Status.Code.CANCELLED); + super.onCancel(); + } + + private void report(final Status.Code code) { + this.responseStatusTiming.accept(code); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerInterceptor.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerInterceptor.java new file mode 100644 index 0000000000..a86218c167 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/MetricCollectingServerInterceptor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2021 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.binder.grpc; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import io.grpc.BindableService; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +/** + * A gRPC server interceptor that will collect metrics using the given {@link MeterRegistry}. + * + *

+ * Usage: + *

+ * + *
+ * Server server = ServerBuilder.forPort(8080)
+ *         .intercept(new MetricCollectingServerInterceptor(meterRegistry))
+ *         .build();
+ *
+ * server.start()
+ * 
+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 1.7.0 + */ +public class MetricCollectingServerInterceptor extends AbstractMetricCollectingInterceptor + implements ServerInterceptor { + + /** + * The total number of requests received + */ + private static final String METRIC_NAME_SERVER_REQUESTS_RECEIVED = "grpc.server.requests.received"; + /** + * The total number of responses sent + */ + private static final String METRIC_NAME_SERVER_RESPONSES_SENT = "grpc.server.responses.sent"; + /** + * The total time taken for the server to complete the call. + */ + private static final String METRIC_NAME_SERVER_PROCESSING_DURATION = "grpc.server.processing.duration"; + + /** + * Creates a new gRPC server interceptor that will collect metrics into the given {@link MeterRegistry}. + * + * @param registry The registry to use. + */ + public MetricCollectingServerInterceptor(final MeterRegistry registry) { + super(registry); + } + + /** + * Creates a new gRPC server interceptor that will collect metrics into the given {@link MeterRegistry} and uses the + * given customizers to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + public MetricCollectingServerInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + super(registry, counterCustomizer, timerCustomizer, eagerInitializedCodes); + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param service The service to initialize the meters for. + * @see #preregisterService(ServerServiceDefinition) + */ + public void preregisterService(final BindableService service) { + preregisterService(service.bindService()); + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param serviceDefinition The service to initialize the meters for. + * @see #preregisterService(ServiceDescriptor) + */ + public void preregisterService(final ServerServiceDefinition serviceDefinition) { + preregisterService(serviceDefinition.getServiceDescriptor()); + } + + @Override + protected Counter newRequestCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_SERVER_REQUESTS_RECEIVED, + "The total number of requests received")) + .register(this.registry); + } + + @Override + protected Counter newResponseCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_SERVER_RESPONSES_SENT, + "The total number of responses sent")) + .register(this.registry); + } + + @Override + protected Function newTimerFunction(final MethodDescriptor method) { + return asTimerFunction(() -> this.timerCustomizer.apply( + prepareTimerFor(method, + METRIC_NAME_SERVER_PROCESSING_DURATION, + "The total time taken for the server to complete the call"))); + } + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata requestHeaders, + final ServerCallHandler next) { + + final MetricSet metrics = metricsFor(call.getMethodDescriptor()); + final Consumer responseStatusTiming = metrics.newProcessingDurationTiming(this.registry); + + final MetricCollectingServerCall monitoringCall = + new MetricCollectingServerCall<>(call, metrics.getResponseCounter()); + + return new MetricCollectingServerCallListener<>( + next.startCall(monitoringCall, requestHeaders), + metrics.getRequestCounter(), + monitoringCall::getResponseCode, + responseStatusTiming); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/grpc/package-info.java b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/package-info.java new file mode 100644 index 0000000000..b7f1279479 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/grpc/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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. + */ +/** + * Collect metrics for grpc-java clients and servers. + * + * Refer to {@link io.micrometer.binder.grpc.MetricCollectingClientInterceptor} and + * {@link io.micrometer.binder.grpc.MetricCollectingServerInterceptor} + * for usage examples. + */ +package io.micrometer.binder.grpc; diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/http/DefaultHttpServletRequestTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/http/DefaultHttpServletRequestTagsProvider.java new file mode 100644 index 0000000000..0d88fd7db0 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/http/DefaultHttpServletRequestTagsProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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.binder.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +/** + * Default {@link HttpServletRequestTagsProvider}. + * + * @author Jon Schneider + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +public class DefaultHttpServletRequestTagsProvider implements HttpServletRequestTagsProvider { + @Override + public Iterable getTags(HttpServletRequest request, HttpServletResponse response) { + return Tags.of(HttpRequestTags.method(request), HttpRequestTags.status(response), HttpRequestTags.outcome(response)); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpRequestTags.java b/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpRequestTags.java new file mode 100644 index 0000000000..100a1e113b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpRequestTags.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020 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.binder.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.util.StringUtils; + +/** + * Tags for HTTP requests. + * + * @author Jon Schneider + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +public class HttpRequestTags { + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + private HttpRequestTags() { + } + + /** + * Creates a {@code method} tag based on the {@link HttpServletRequest#getMethod() + * method} of the given {@code request}. + * @param request the request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(HttpServletRequest request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code response}. + * @param response the HTTP response + * @return the status tag derived from the status of the response + */ + public static Tag status(HttpServletResponse response) { + return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_UNKNOWN; + } + + /** + * Creates a {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * @param exception the exception, may be {@code null} + * @return the exception tag derived from the exception + */ + public static Tag exception(Throwable exception) { + if (exception != null) { + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotBlank(simpleName) ? simpleName : exception.getClass().getName()); + } + return EXCEPTION_NONE; + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code response}. + * @param response the HTTP response + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(HttpServletResponse response) { + Outcome outcome = (response != null) ? Outcome.forStatus(response.getStatus()) : Outcome.UNKNOWN; + return outcome.asTag(); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpServletRequestTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpServletRequestTagsProvider.java new file mode 100644 index 0000000000..6ade68094e --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/http/HttpServletRequestTagsProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.binder.http; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; + +/** + * Provides {@link Tag Tags} for HTTP Servlet request handling. + * + * @author Jon Schneider + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +@FunctionalInterface +public interface HttpServletRequestTagsProvider { + /** + * Provides tags to be associated with metrics for the given {@code request} and + * {@code response} exchange. + * @param request the request + * @param response the response + * @return tags to associate with metrics for the request and response exchange + */ + Iterable getTags(HttpServletRequest request, HttpServletResponse response); +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/http/Outcome.java b/micrometer-binders/src/main/java/io/micrometer/binder/http/Outcome.java new file mode 100644 index 0000000000..c7789a960c --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/http/Outcome.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 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.binder.http; + +import io.micrometer.core.instrument.Tag; + +/** + * The outcome of an HTTP request. + * + * @author Andy Wilkinson + * @since 1.4.0 + */ +public enum Outcome { + + /** + * Outcome of the request was informational. + */ + INFORMATIONAL, + + /** + * Outcome of the request was success. + */ + SUCCESS, + + /** + * Outcome of the request was redirection. + */ + REDIRECTION, + + /** + * Outcome of the request was client error. + */ + CLIENT_ERROR, + + /** + * Outcome of the request was server error. + */ + SERVER_ERROR, + + /** + * Outcome of the request was unknown. + */ + UNKNOWN; + + private final Tag tag; + + Outcome() { + this.tag = Tag.of("outcome", name()); + } + + /** + * Returns the {@code Outcome} as a {@link Tag} named {@code outcome}. + * + * @return the {@code outcome} {@code Tag} + */ + public Tag asTag() { + return this.tag; + } + + /** + * Return the {@code Outcome} for the given HTTP {@code status} code. + * + * @param status the HTTP status code + * @return the matching Outcome + */ + public static Outcome forStatus(int status) { + if (status >= 100 && status < 200) { + return INFORMATIONAL; + } else if (status >= 200 && status < 300) { + return SUCCESS; + } else if (status >= 300 && status < 400) { + return REDIRECTION; + } else if (status >= 400 && status < 500) { + return CLIENT_ERROR; + } else if (status >= 500 && status < 600) { + return SERVER_ERROR; + } + return UNKNOWN; + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/http/package-info.java b/micrometer-binders/src/main/java/io/micrometer/binder/http/package-info.java new file mode 100644 index 0000000000..3e00cc3d2a --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/http/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright 2020 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.binder.http; diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/DefaultUriMapper.java b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/DefaultUriMapper.java new file mode 100644 index 0000000000..31dd34a9ce --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/DefaultUriMapper.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 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.binder.httpcomponents; + +import java.util.function.Function; + +import org.apache.http.Header; +import org.apache.http.HttpRequest; + +/** + * Extracts the URI pattern from the predefined request header, {@value DefaultUriMapper#URI_PATTERN_HEADER} if available. + * + * @author Benjamin Hubert + * @since 1.4.0 + */ +public class DefaultUriMapper implements Function { + + /** + * Header name for URI pattern. + */ + public static final String URI_PATTERN_HEADER = "URI_PATTERN"; + + @Override + public String apply(HttpRequest httpRequest) { + Header uriPattern = httpRequest.getLastHeader(URI_PATTERN_HEADER); + if (uriPattern != null && uriPattern.getValue() != null) { + return uriPattern.getValue(); + } + return "UNKNOWN"; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/HttpContextUtils.java b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/HttpContextUtils.java new file mode 100644 index 0000000000..ccbee0d9a4 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/HttpContextUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 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.binder.httpcomponents; + +import io.micrometer.core.instrument.Tags; +import org.apache.http.HttpHost; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.protocol.HttpContext; + +class HttpContextUtils { + static Tags generateTagsForRoute(HttpContext context) { + String targetScheme = "UNKNOWN"; + String targetHost = "UNKNOWN"; + String targetPort = "UNKNOWN"; + Object routeAttribute = context.getAttribute("http.route"); + if (routeAttribute instanceof HttpRoute) { + HttpHost host = ((HttpRoute) routeAttribute).getTargetHost(); + targetScheme = host.getSchemeName(); + targetHost = host.getHostName(); + targetPort = String.valueOf(host.getPort()); + } + return Tags.of( + "target.scheme", targetScheme, + "target.host", targetHost, + "target.port", targetPort + ); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptor.java b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptor.java new file mode 100644 index 0000000000..142e33df2b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptor.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020 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.binder.httpcomponents; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.protocol.HttpContext; + +/** + * Provides {@link HttpRequestInterceptor} and {@link HttpResponseInterceptor} for + * configuring with an {@link org.apache.http.nio.client.HttpAsyncClient}. Usage example: + *
{@code
+ *     MicrometerHttpClientInterceptor interceptor = new MicrometerHttpClientInterceptor(registry,
+ *             request -> request.getRequestLine().getUri(),
+ *             Tags.empty(),
+ *             true);
+ *
+ *     CloseableHttpAsyncClient httpAsyncClient = HttpAsyncClients.custom()
+ *             .addInterceptorFirst(interceptor.getRequestInterceptor())
+ *             .addInterceptorLast(interceptor.getResponseInterceptor())
+ *             .build();
+ * }
+ * + * @author Jon Schneider + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +public class MicrometerHttpClientInterceptor { + private static final String METER_NAME = "httpcomponents.httpclient.request"; + + private final Map timerByHttpContext = new ConcurrentHashMap<>(); + + private final HttpRequestInterceptor requestInterceptor; + private final HttpResponseInterceptor responseInterceptor; + + /** + * Create a {@code MicrometerHttpClientInterceptor} instance. + * + * @param meterRegistry meter registry to bind + * @param uriMapper URI mapper to create {@code uri} tag + * @param extraTags extra tags + * @param exportTagsForRoute whether to export tags for route + */ + public MicrometerHttpClientInterceptor(MeterRegistry meterRegistry, + Function uriMapper, + Iterable extraTags, + boolean exportTagsForRoute) { + this.requestInterceptor = (request, context) -> timerByHttpContext.put(context, Timer.resource(meterRegistry, METER_NAME) + .tags("method", request.getRequestLine().getMethod(), "uri", uriMapper.apply(request))); + + this.responseInterceptor = (response, context) -> { + timerByHttpContext.remove(context) + .tag("status", Integer.toString(response.getStatusLine().getStatusCode())) + .tags(exportTagsForRoute ? HttpContextUtils.generateTagsForRoute(context) : Tags.empty()) + .tags(extraTags) + .close(); + }; + } + + /** + * Create a {@code MicrometerHttpClientInterceptor} instance with {@link DefaultUriMapper}. + * + * @param meterRegistry meter registry to bind + * @param extraTags extra tags + * @param exportTagsForRoute whether to export tags for route + */ + public MicrometerHttpClientInterceptor(MeterRegistry meterRegistry, + Iterable extraTags, + boolean exportTagsForRoute) { + this(meterRegistry, new DefaultUriMapper(), extraTags, exportTagsForRoute); + } + + public HttpRequestInterceptor getRequestInterceptor() { + return requestInterceptor; + } + + public HttpResponseInterceptor getResponseInterceptor() { + return responseInterceptor; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutor.java b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutor.java new file mode 100644 index 0000000000..f8892baa3d --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutor.java @@ -0,0 +1,204 @@ +/* + * Copyright 2019 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.binder.httpcomponents; + +import java.io.IOException; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Function; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import org.apache.http.HttpClientConnection; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpRequestExecutor; + +/** + * This HttpRequestExecutor tracks the request duration of every request, that + * goes through an {@link org.apache.http.client.HttpClient}. It must be + * registered as request executor when creating the HttpClient instance. + * For example: + * + *
+ *     HttpClientBuilder.create()
+ *         .setRequestExecutor(MicrometerHttpRequestExecutor
+ *                 .builder(meterRegistry)
+ *                 .build())
+ *         .build();
+ * 
+ * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + * @author Tommy Ludwig + * @since 1.2.0 + */ +@Incubating(since = "1.2.0") +public class MicrometerHttpRequestExecutor extends HttpRequestExecutor { + + /** + * Default header name for URI pattern. + * @deprecated use {@link DefaultUriMapper#URI_PATTERN_HEADER} since 1.4.0 + */ + @Deprecated + public static final String DEFAULT_URI_PATTERN_HEADER = DefaultUriMapper.URI_PATTERN_HEADER; + + private static final String METER_NAME = "httpcomponents.httpclient.request"; + + private static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN"); + private static final Tag STATUS_CLIENT_ERROR = Tag.of("status", "CLIENT_ERROR"); + private static final Tag STATUS_IO_ERROR = Tag.of("status", "IO_ERROR"); + + private final MeterRegistry registry; + private final Function uriMapper; + private final Iterable extraTags; + private final boolean exportTagsForRoute; + + /** + * Use {@link #builder(MeterRegistry)} to create an instance of this class. + */ + private MicrometerHttpRequestExecutor(int waitForContinue, + MeterRegistry registry, + Function uriMapper, + Iterable extraTags, + boolean exportTagsForRoute) { + super(waitForContinue); + this.registry = Optional.ofNullable(registry).orElseThrow(() -> new IllegalArgumentException("registry is required but has been initialized with null")); + this.uriMapper = Optional.ofNullable(uriMapper).orElseThrow(() -> new IllegalArgumentException("uriMapper is required but has been initialized with null")); + this.extraTags = Optional.ofNullable(extraTags).orElse(Collections.emptyList()); + this.exportTagsForRoute = exportTagsForRoute; + } + + /** + * Use this method to create an instance of {@link MicrometerHttpRequestExecutor}. + * + * @param registry The registry to register the metrics to. + * @return An instance of the builder, which allows further configuration of + * the request executor. + */ + public static Builder builder(MeterRegistry registry) { + return new Builder(registry); + } + + @Override + public HttpResponse execute(HttpRequest request, HttpClientConnection conn, HttpContext context) throws IOException, HttpException { + Timer.Sample timerSample = Timer.start(registry); + + Tag method = Tag.of("method", request.getRequestLine().getMethod()); + Tag uri = Tag.of("uri", uriMapper.apply(request)); + Tag status = STATUS_UNKNOWN; + + Tags routeTags = exportTagsForRoute ? HttpContextUtils.generateTagsForRoute(context) : Tags.empty(); + + try { + HttpResponse response = super.execute(request, conn, context); + status = response != null ? Tag.of("status", Integer.toString(response.getStatusLine().getStatusCode())) : STATUS_CLIENT_ERROR; + return response; + } catch (IOException | HttpException | RuntimeException e) { + status = STATUS_IO_ERROR; + throw e; + } finally { + Iterable tags = Tags.of(extraTags) + .and(routeTags) + .and(uri, method, status); + + timerSample.stop(Timer.builder(METER_NAME) + .description("Duration of Apache HttpClient request execution") + .tags(tags) + .register(registry)); + } + } + + public static class Builder { + private final MeterRegistry registry; + private int waitForContinue = HttpRequestExecutor.DEFAULT_WAIT_FOR_CONTINUE; + private Iterable tags = Collections.emptyList(); + private Function uriMapper = new DefaultUriMapper(); + private boolean exportTagsForRoute = false; + + Builder(MeterRegistry registry) { + this.registry = registry; + } + + /** + * @param waitForContinue Overrides the wait for continue time for this + * request executor. See {@link HttpRequestExecutor} + * for details. + * @return This builder instance. + */ + public Builder waitForContinue(int waitForContinue) { + this.waitForContinue = waitForContinue; + return this; + } + + /** + * @param tags Additional tags which should be exposed with every value. + * @return This builder instance. + */ + public Builder tags(Iterable tags) { + this.tags = tags; + return this; + } + + /** + * Allows to register a mapping function for exposing request URIs. Be + * careful, exposing request URIs could result in a huge number of tag + * values, which could cause problems in your meter registry. + * + * By default, this feature is almost disabled. It only exposes values + * of the {@value DefaultUriMapper#URI_PATTERN_HEADER} HTTP header. + * + * @param uriMapper A mapper that allows mapping and exposing request + * paths. + * @return This builder instance. + * @see DefaultUriMapper + */ + public Builder uriMapper(Function uriMapper) { + this.uriMapper = uriMapper; + return this; + } + + /** + * Allows to expose the target scheme, host and port with every metric. + * Be careful with enabling this feature: If your client accesses a huge + * number of remote servers, this would result in a huge number of tag + * values, which could cause cardinality problems. + * + * By default, this feature is disabled. + * + * @param exportTagsForRoute Set this to true, if the metrics should be + * tagged with the target route. + * @return This builder instance. + */ + public Builder exportTagsForRoute(boolean exportTagsForRoute) { + this.exportTagsForRoute = exportTagsForRoute; + return this; + } + + /** + * @return Creates an instance of {@link MicrometerHttpRequestExecutor} + * with all the configured properties. + */ + public MicrometerHttpRequestExecutor build() { + return new MicrometerHttpRequestExecutor(waitForContinue, registry, uriMapper, tags, exportTagsForRoute); + } + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java new file mode 100644 index 0000000000..030ce728c7 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 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.binder.httpcomponents; + +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.micrometer.core.lang.NonNull; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.pool.ConnPoolControl; + +/** + * Collects metrics from a {@link ConnPoolControl}, for example {@link org.apache.http.impl.conn.PoolingHttpClientConnectionManager} + * for synchronous HTTP clients or {@link org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager} for asynchronous HTTP clients. + *
{@code
+ *     Server server = new Server(new InstrumentedQueuedThreadPool(registry, Tags.empty()));
+ *     // ...
+ * }
+ * + * @since 1.1.0 + * @see JettyServerThreadPoolMetrics + */ +public class InstrumentedQueuedThreadPool extends QueuedThreadPool { + + private final MeterRegistry registry; + private final Iterable tags; + + /** + * Default values for the instrumented thread pool. + * + * @param registry where metrics will be bound + * @param tags tags to apply to metrics bound from this + */ + public InstrumentedQueuedThreadPool(MeterRegistry registry, Iterable tags) { + this.registry = registry; + this.tags = tags; + } + + /** + * Instrumented thread pool. + * + * @param registry where metrics will be bound + * @param tags tags to apply to metrics bound from this + * @param maxThreads maximum threads for the thread pool + * @since 1.5.0 + */ + public InstrumentedQueuedThreadPool(MeterRegistry registry, Iterable tags, int maxThreads) { + super(maxThreads); + this.registry = registry; + this.tags = tags; + } + + /** + * Instrumented thread pool. + * + * @param registry where metrics will be bound + * @param tags tags to apply to metrics bound from this + * @param maxThreads maximum threads for the thread pool + * @param minThreads minimum threads for the thread pool + * @since 1.5.0 + */ + public InstrumentedQueuedThreadPool(MeterRegistry registry, Iterable tags, int maxThreads, int minThreads) { + super(maxThreads, minThreads); + this.registry = registry; + this.tags = tags; + } + + /** + * Instrumented thread pool. + * + * @param registry where metrics will be bound + * @param tags tags to apply to metrics bound from this + * @param maxThreads maximum threads for the thread pool + * @param minThreads minimum threads for the thread pool + * @param idleTimeout timeout for idle threads in pool + * @since 1.5.0 + */ + public InstrumentedQueuedThreadPool(MeterRegistry registry, + Iterable tags, + int maxThreads, + int minThreads, + int idleTimeout) { + super(maxThreads, minThreads, idleTimeout); + this.registry = registry; + this.tags = tags; + } + + /** + * Instrumented thread pool. + * + * @param registry where metrics will be bound + * @param tags tags to apply to metrics bound from this + * @param maxThreads maximum threads for the thread pool + * @param minThreads minimum threads for the thread pool + * @param idleTimeout timeout for idle threads in pool + * @param queue backing queue for thread pool tasks + * @since 1.5.0 + */ + public InstrumentedQueuedThreadPool(MeterRegistry registry, + Iterable tags, + int maxThreads, + int minThreads, + int idleTimeout, + BlockingQueue queue) { + super(maxThreads, minThreads, idleTimeout, queue); + this.registry = registry; + this.tags = tags; + } + + @Override + protected void doStart() throws Exception { + super.doStart(); + JettyServerThreadPoolMetrics threadPoolMetrics = new JettyServerThreadPoolMetrics(this, tags); + threadPoolMetrics.bindTo(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientMetrics.java new file mode 100644 index 0000000000..6535e1bbd8 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientMetrics.java @@ -0,0 +1,122 @@ +/* + * Copyright 2020 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.binder.jetty; + +import java.util.Optional; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.internal.OnlyOnceLoggingDenyMeterFilter; +import org.eclipse.jetty.client.api.ContentProvider; +import org.eclipse.jetty.client.api.Request; + +/** + * Provides request metrics for Jetty {@link org.eclipse.jetty.client.HttpClient}, + * configured as a {@link Request.Listener Request.Listener}. + * Incubating in case there emerges a better way to handle path variable detection. + * + * @author Jon Schneider + * @since 1.5.0 + */ +@Incubating(since = "1.5.0") +public class JettyClientMetrics implements Request.Listener { + private final MeterRegistry registry; + private final JettyClientTagsProvider tagsProvider; + private final String timingMetricName; + private final String contentSizeMetricName; + + protected JettyClientMetrics(MeterRegistry registry, JettyClientTagsProvider tagsProvider, String timingMetricName, String contentSizeMetricName, int maxUriTags) { + this.registry = registry; + this.tagsProvider = tagsProvider; + this.timingMetricName = timingMetricName; + this.contentSizeMetricName = contentSizeMetricName; + + MeterFilter timingMetricDenyFilter = new OnlyOnceLoggingDenyMeterFilter( + () -> String.format("Reached the maximum number of URI tags for '%s'.", timingMetricName)); + MeterFilter contentSizeMetricDenyFilter = new OnlyOnceLoggingDenyMeterFilter( + () -> String.format("Reached the maximum number of URI tags for '%s'.", contentSizeMetricName)); + registry.config() + .meterFilter(MeterFilter.maximumAllowableTags( + this.timingMetricName, "uri", maxUriTags, timingMetricDenyFilter)) + .meterFilter(MeterFilter.maximumAllowableTags( + this.contentSizeMetricName, "uri", maxUriTags, contentSizeMetricDenyFilter)); + } + + @Override + public void onQueued(Request request) { + Timer.Sample sample = Timer.start(registry); + + request.onComplete(result -> { + long requestLength = Optional.ofNullable(result.getRequest().getContent()) + .map(ContentProvider::getLength) + .orElse(0L); + Iterable httpRequestTags = tagsProvider.httpRequestTags(result); + if (requestLength >= 0) { + DistributionSummary.builder(contentSizeMetricName) + .description("Content sizes for Jetty HTTP client requests") + .tags(httpRequestTags) + .register(registry) + .record(requestLength); + } + + sample.stop(Timer.builder(timingMetricName) + .description("Jetty HTTP client request timing") + .tags(httpRequestTags) + .register(registry)); + }); + } + + public static Builder builder(MeterRegistry registry, JettyClientTagsProvider tagsProvider) { + return new Builder(registry, tagsProvider); + } + + public static class Builder { + private final MeterRegistry registry; + private final JettyClientTagsProvider tagsProvider; + + private String timingMetricName = "jetty.client.requests"; + private String contentSizeMetricName = "jetty.client.request.size"; + private int maxUriTags = 1000; + + Builder(MeterRegistry registry, JettyClientTagsProvider tagsProvider) { + this.registry = registry; + this.tagsProvider = tagsProvider; + } + + public Builder timingMetricName(String metricName) { + this.timingMetricName = metricName; + return this; + } + + public Builder contentSizeMetricName(String metricName) { + this.contentSizeMetricName = metricName; + return this; + } + + public Builder maxUriTags(int maxUriTags) { + this.maxUriTags = maxUriTags; + return this; + } + + public JettyClientMetrics build() { + return new JettyClientMetrics(registry, tagsProvider, timingMetricName, contentSizeMetricName, maxUriTags); + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTags.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTags.java new file mode 100644 index 0000000000..5257ce6940 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTags.java @@ -0,0 +1,154 @@ +/* + * Copyright 2020 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.binder.jetty; + +import java.util.function.Function; +import java.util.regex.Pattern; + +import io.micrometer.binder.http.Outcome; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.util.StringUtils; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.http.HttpStatus; + +/** + * Factory methods for {@link Tag Tags} associated with a request-response exchange that + * is handled by Jetty {@link org.eclipse.jetty.client.HttpClient}. + * + * @author Jon Schneider + * @since 1.5.0 + */ +public final class JettyClientTags { + + private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); + + private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION"); + + private static final Tag URI_ROOT = Tag.of("uri", "root"); + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + private static final Tag HOST_UNKNOWN = Tag.of("host", "UNKNOWN"); + + private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); + + private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + private JettyClientTags() { + } + + /** + * Creates a {@code method} tag based on the {@link Request#getMethod() + * method} of the given {@code request}. + * + * @param request the request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(Request request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code host} tag based on the {@link Request#getHost()} of the given {@code request}. + * + * @param request the request + * @return the host tag derived from request + * @since 1.7.0 + */ + public static Tag host(Request request) { + return (request != null) ? Tag.of("host", request.getHost()) : HOST_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code result}. + * + * @param result the request result + * @return the status tag derived from the status of the response + */ + public static Tag status(Result result) { + return Tag.of("status", Integer.toString(result.getResponse().getStatus())); + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code result}. + * {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 responses. + * + * @param result the request result + * @param successfulUriPattern successful URI pattern + * @return the uri tag derived from the request result + */ + public static Tag uri(Result result, Function successfulUriPattern) { + Response response = result.getResponse(); + if (response != null) { + int status = response.getStatus(); + if (HttpStatus.isRedirection(status)) { + return URI_REDIRECTION; + } + if (status == 404) { + return URI_NOT_FOUND; + } + } + + String matchingPattern = successfulUriPattern.apply(result); + matchingPattern = MULTIPLE_SLASH_PATTERN.matcher(matchingPattern).replaceAll("/"); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + matchingPattern = TRAILING_SLASH_PATTERN.matcher(matchingPattern).replaceAll(""); + return Tag.of("uri", matchingPattern); + } + + /** + * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * + * @param result the request result + * @return the exception tag derived from the exception + */ + public static Tag exception(Result result) { + Throwable exception = result.getFailure(); + if (exception == null) { + return EXCEPTION_NONE; + } + if (result.getResponse() != null) { + int status = result.getResponse().getStatus(); + if (status == 404 || HttpStatus.isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName + : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code result}. + * + * @param result the request result + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(Result result) { + return Outcome.forStatus(result.getResponse().getStatus()).asTag(); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTagsProvider.java new file mode 100644 index 0000000000..9f246813fc --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyClientTagsProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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.binder.jetty; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.eclipse.jetty.client.api.Result; + +/** + * Provides {@link Tag Tags} for Jetty {@link org.eclipse.jetty.client.HttpClient} request metrics. + * Incubating in case there emerges a better way to handle path variable detection. + * + * @author Jon Schneider + * @since 1.5.0 + */ +@Incubating(since = "1.5.0") +public interface JettyClientTagsProvider { + + /** + * Provides tags to be associated with metrics for the given client request. + * + * @param result the request result + * @return tags to associate with metrics recorded for the request + */ + default Iterable httpRequestTags(Result result) { + return Tags.of(JettyClientTags.method(result.getRequest()), + JettyClientTags.host(result.getRequest()), + JettyClientTags.uri(result, this::uriPattern), + JettyClientTags.exception(result), JettyClientTags.status(result), + JettyClientTags.outcome(result)); + } + + /** + * For client metric to be usefully aggregable, we must be able to time everything that goes to a certain + * endpoint, regardless of the parameters to that endpoint. + * + * @param result The result which also contains the original request. + * @return A URI pattern with path variables and query parameter unsubstituted. + */ + String uriPattern(Result result); +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyConnectionMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyConnectionMetrics.java new file mode 100644 index 0000000000..19ddf9c3ff --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyConnectionMetrics.java @@ -0,0 +1,191 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.util.HashMap; +import java.util.Map; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +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.Timer; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; +import io.micrometer.core.instrument.distribution.TimeWindowMax; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +/** + * Jetty connection metrics.

+ *

+ * Usage example: + * + *

{@code
+ * MeterRegistry registry = ...;
+ * Server server = new Server(0);
+ * Connector connector = new ServerConnector(server);
+ * connector.addBean(new JettyConnectionMetrics(registry));
+ * server.setConnectors(new Connector[] { connector });
+ * }
+ * + * Alternatively, configure on all connectors with {@link JettyConnectionMetrics#addToAllConnectors(Server, MeterRegistry, Iterable)}. + * + * @author Jon Schneider + * @since 1.4.0 + */ +public class JettyConnectionMetrics extends AbstractLifeCycle implements Connection.Listener { + private final MeterRegistry registry; + private final Iterable tags; + + private final Object connectionSamplesLock = new Object(); + private final Map connectionSamples = new HashMap<>(); + + private final Counter messagesIn; + private final Counter messagesOut; + private final DistributionSummary bytesIn; + private final DistributionSummary bytesOut; + + private final TimeWindowMax maxConnections; + + public JettyConnectionMetrics(MeterRegistry registry) { + this(registry, Tags.empty()); + } + + public JettyConnectionMetrics(MeterRegistry registry, Iterable tags) { + this.registry = registry; + this.tags = tags; + + this.messagesIn = Counter.builder("jetty.connections.messages.in") + .baseUnit(BaseUnits.MESSAGES) + .description("Messages received by tracked connections") + .tags(tags) + .register(registry); + + this.messagesOut = Counter.builder("jetty.connections.messages.out") + .baseUnit(BaseUnits.MESSAGES) + .description("Messages sent by tracked connections") + .tags(tags) + .register(registry); + + this.bytesIn = DistributionSummary.builder("jetty.connections.bytes.in") + .baseUnit(BaseUnits.BYTES) + .description("Bytes received by tracked connections") + .tags(tags) + .register(registry); + + this.bytesOut = DistributionSummary.builder("jetty.connections.bytes.out") + .baseUnit(BaseUnits.BYTES) + .description("Bytes sent by tracked connections") + .tags(tags) + .register(registry); + + this.maxConnections = new TimeWindowMax(registry.config().clock(), DistributionStatisticConfig.DEFAULT); + + Gauge.builder("jetty.connections.max", this, jcm -> jcm.maxConnections.poll()) + .strongReference(true) + .baseUnit(BaseUnits.CONNECTIONS) + .description("The maximum number of observed connections over a rolling 2-minute interval") + .tags(tags) + .register(registry); + + Gauge.builder("jetty.connections.current", this, jcm -> jcm.connectionSamples.size()) + .strongReference(true) + .baseUnit(BaseUnits.CONNECTIONS) + .description("The current number of open Jetty connections") + .tags(tags) + .register(registry); + } + + /** + * Create a {@code JettyConnectionMetrics} instance. {@link Connector#getName()} will be used for + * {@literal connector.name} tag. + * + * @param registry registry to use + * @param connector connector to instrument + * @since 1.8.0 + */ + public JettyConnectionMetrics(MeterRegistry registry, Connector connector) { + this(registry, connector, Tags.empty()); + } + + /** + * Create a {@code JettyConnectionMetrics} instance. {@link Connector#getName()} will be used for + * {@literal connector.name} tag. + * + * @param registry registry to use + * @param connector connector to instrument + * @param tags tags to add to metrics + * @since 1.8.0 + */ + public JettyConnectionMetrics(MeterRegistry registry, Connector connector, Iterable tags) { + this(registry, getConnectorNameTag(connector).and(tags)); + } + + private static Tags getConnectorNameTag(Connector connector) { + String name = connector.getName(); + return Tags.of("connector.name", name != null ? name : "unnamed"); + } + + @Override + public void onOpened(Connection connection) { + Timer.Sample started = Timer.start(registry); + synchronized (connectionSamplesLock) { + connectionSamples.put(connection, started); + maxConnections.record(connectionSamples.size()); + } + } + + @Override + public void onClosed(Connection connection) { + Timer.Sample sample; + synchronized (connectionSamplesLock) { + sample = connectionSamples.remove(connection); + } + + if (sample != null) { + String serverOrClient = connection instanceof HttpConnection ? "server" : "client"; + sample.stop(Timer.builder("jetty.connections.request") + .description("Jetty client or server requests") + .tag("type", serverOrClient) + .tags(tags) + .register(registry)); + } + + messagesIn.increment(connection.getMessagesIn()); + messagesOut.increment(connection.getMessagesOut()); + + bytesIn.record(connection.getBytesIn()); + bytesOut.record(connection.getBytesOut()); + } + + public static void addToAllConnectors(Server server, MeterRegistry registry, Iterable tags) { + for (Connector connector : server.getConnectors()) { + if (connector != null) { + connector.addBean(new JettyConnectionMetrics(registry, connector, tags)); + } + } + } + + public static void addToAllConnectors(Server server, MeterRegistry registry) { + addToAllConnectors(server, registry, Tags.empty()); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetrics.java new file mode 100644 index 0000000000..fa032447f9 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetrics.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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.binder.jetty; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.MeterBinder; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool.SizedThreadPool; + +/** + * {@link MeterBinder} for Jetty {@link ThreadPool}. + *

+ * Pass the {@link ThreadPool} used with the Jetty {@link org.eclipse.jetty.server.Server Server}. For example: + *

+ *     {@code
+ *     QueuedThreadPool threadPool = new QueuedThreadPool();
+ *     Server server = new Server(threadPool);
+ *     new JettyServerThreadPoolMetrics(threadPool, tags).bindTo(registry);
+ *     }
+ * 
+ * + * @author Manabu Matsuzaki + * @author Andy Wilkinson + * @author Johnny Lim + * @since 1.1.0 + * @see InstrumentedQueuedThreadPool + */ +public class JettyServerThreadPoolMetrics implements MeterBinder { + + private final ThreadPool threadPool; + + private final Iterable tags; + + public JettyServerThreadPoolMetrics(ThreadPool threadPool, Iterable tags) { + this.threadPool = threadPool; + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + if (threadPool instanceof SizedThreadPool) { + SizedThreadPool sizedThreadPool = (SizedThreadPool) threadPool; + Gauge.builder("jetty.threads.config.min", sizedThreadPool, SizedThreadPool::getMinThreads) + .description("The minimum number of threads in the pool") + .tags(tags).register(registry); + Gauge.builder("jetty.threads.config.max", sizedThreadPool, SizedThreadPool::getMaxThreads) + .description("The maximum number of threads in the pool") + .tags(tags).register(registry); + if (threadPool instanceof QueuedThreadPool) { + QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool; + Gauge.builder("jetty.threads.busy", queuedThreadPool, QueuedThreadPool::getBusyThreads) + .description("The number of busy threads in the pool") + .tags(tags).register(registry); + Gauge.builder("jetty.threads.jobs", queuedThreadPool, QueuedThreadPool::getQueueSize) + .description("Number of jobs queued waiting for a thread") + .tags(tags).register(registry); + } + } + Gauge.builder("jetty.threads.current", threadPool, ThreadPool::getThreads) + .description("The total number of threads in the pool") + .tags(tags).register(registry); + Gauge.builder("jetty.threads.idle", threadPool, ThreadPool::getIdleThreads) + .description("The number of idle threads in the pool").tags(tags) + .register(registry); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettySslHandshakeMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettySslHandshakeMetrics.java new file mode 100644 index 0000000000..22e9b0256e --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/JettySslHandshakeMetrics.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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.binder.jetty; + +import javax.net.ssl.SSLSession; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.BaseUnits; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; + +/** + * Jetty SSL/TLS handshake metrics.

+ *

+ * Usage example: + * + *

{@code
+ * MeterRegistry registry = ...;
+ * Server server = new Server(0);
+ * Connector connector = new ServerConnector(server);
+ * connector.addBean(new JettySslHandshakeMetrics(registry));
+ * server.setConnectors(new Connector[] { connector });
+ * }
+ * + * Alternatively, configure on all connectors with {@link JettySslHandshakeMetrics#addToAllConnectors(Server, MeterRegistry, Iterable)}. + * + * @author John Karp + * @author Johnny Lim + * @since 1.5.0 + */ +public class JettySslHandshakeMetrics implements SslHandshakeListener { + private static final String METER_NAME = "jetty.ssl.handshakes"; + private static final String DESCRIPTION = "SSL/TLS handshakes"; + private static final String TAG_RESULT = "result"; + private static final String TAG_PROTOCOL = "protocol"; + private static final String TAG_CIPHER_SUITE = "ciphersuite"; + private static final String TAG_VALUE_UNKNOWN = "unknown"; + + private final MeterRegistry registry; + private final Iterable tags; + + private final Counter handshakesFailed; + + public JettySslHandshakeMetrics(MeterRegistry registry) { + this(registry, Tags.empty()); + } + + public JettySslHandshakeMetrics(MeterRegistry registry, Iterable tags) { + this.registry = registry; + this.tags = tags; + + this.handshakesFailed = Counter.builder(METER_NAME) + .baseUnit(BaseUnits.EVENTS) + .description(DESCRIPTION) + .tag(TAG_RESULT, "failed") + .tag(TAG_PROTOCOL, TAG_VALUE_UNKNOWN) + .tag(TAG_CIPHER_SUITE, TAG_VALUE_UNKNOWN) + .tags(tags) + .register(registry); + } + + /** + * Create a {@code JettySslHandshakeMetrics} instance. {@link Connector#getName()} will be used for + * {@literal connector.name} tag. + * + * @param registry registry to use + * @param connector connector to instrument + * @since 1.8.0 + */ + public JettySslHandshakeMetrics(MeterRegistry registry, Connector connector) { + this(registry, connector, Tags.empty()); + } + + /** + * Create a {@code JettySslHandshakeMetrics} instance. {@link Connector#getName()} will be used for + * {@literal connector.name} tag. + * + * @param registry registry to use + * @param connector connector to instrument + * @param tags tags to add to metrics + * @since 1.8.0 + */ + public JettySslHandshakeMetrics(MeterRegistry registry, Connector connector, Iterable tags) { + this(registry, getConnectorNameTag(connector).and(tags)); + } + + private static Tags getConnectorNameTag(Connector connector) { + String name = connector.getName(); + return Tags.of("connector.name", name != null ? name : "unnamed"); + } + + @Override + public void handshakeSucceeded(Event event) { + SSLSession session = event.getSSLEngine().getSession(); + Counter.builder(METER_NAME) + .baseUnit(BaseUnits.EVENTS) + .description(DESCRIPTION) + .tag(TAG_RESULT, "succeeded") + .tag(TAG_PROTOCOL, session.getProtocol()) + .tag(TAG_CIPHER_SUITE, session.getCipherSuite()) + .tags(tags) + .register(registry) + .increment(); + } + + @Override + public void handshakeFailed(Event event, Throwable failure) { + handshakesFailed.increment(); + } + + public static void addToAllConnectors(Server server, MeterRegistry registry, Iterable tags) { + for (Connector connector : server.getConnectors()) { + if (connector != null) { + connector.addBean(new JettySslHandshakeMetrics(registry, connector, tags)); + } + } + } + + public static void addToAllConnectors(Server server, MeterRegistry registry) { + addToAllConnectors(server, registry, Tags.empty()); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/OnCompletionAsyncListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/OnCompletionAsyncListener.java new file mode 100644 index 0000000000..7c67444848 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/OnCompletionAsyncListener.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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.binder.jetty; + +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; + +/** + * {@link AsyncListener} that calls back to the handler. This class + * uses only object references to work around + *
WFLY-13345 + */ +class OnCompletionAsyncListener implements AsyncListener { + + private final Object handler; + + OnCompletionAsyncListener(Object handler) { + this.handler = handler; + } + + @Override + public void onTimeout(AsyncEvent event) { + ((TimedHandler) handler).onAsyncTimeout(event); + } + + @Override + public void onStartAsync(AsyncEvent event) { + event.getAsyncContext().addListener(this); + } + + @Override + public void onError(AsyncEvent event) { + } + + @Override + public void onComplete(AsyncEvent event) { + ((TimedHandler) handler).onAsyncComplete(event); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/TimedHandler.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/TimedHandler.java new file mode 100644 index 0000000000..78f90aea3d --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/TimedHandler.java @@ -0,0 +1,227 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.binder.http.DefaultHttpServletRequestTagsProvider; +import io.micrometer.binder.http.HttpServletRequestTagsProvider; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.AsyncContextEvent; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpChannelState; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.FutureCallback; +import org.eclipse.jetty.util.component.Graceful; + +/** + * Adapted from Jetty's StatisticsHandler. + * + * @author Jon Schneider + * @since 1.4.0 + */ +@NonNullApi +@NonNullFields +public class TimedHandler extends HandlerWrapper implements Graceful { + private static final String SAMPLE_REQUEST_TIMER_ATTRIBUTE = "__micrometer_timer_sample"; + private static final String SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE = "__micrometer_ltt_sample"; + + private final MeterRegistry registry; + private final Iterable tags; + private final HttpServletRequestTagsProvider tagsProvider; + + private final Shutdown shutdown = new Shutdown() { + @Override + protected FutureCallback newShutdownCallback() { + return TimedHandler.this.newShutdownCallback(); + } + }; + + private final LongTaskTimer openRequests; + private final Counter asyncDispatches; + private final Counter asyncExpires; + private final AtomicInteger asyncWaits = new AtomicInteger(); + + public TimedHandler(MeterRegistry registry, Iterable tags) { + this(registry, tags, new DefaultHttpServletRequestTagsProvider()); + } + + public TimedHandler(MeterRegistry registry, Iterable tags, HttpServletRequestTagsProvider tagsProvider) { + this.registry = registry; + this.tags = tags; + this.tagsProvider = tagsProvider; + + this.openRequests = LongTaskTimer.builder("jetty.server.dispatches.open") + .description("Jetty dispatches that are currently in progress") + .tags(tags) + .register(registry); + + this.asyncDispatches = Counter.builder("jetty.server.async.dispatches") + .description("Asynchronous dispatches") + .tags(tags) + .register(registry); + + this.asyncExpires = Counter.builder("jetty.server.async.expires") + .description("Asynchronous operations that timed out before completing") + .tags(tags) + .register(registry); + + Gauge.builder("jetty.server.async.waits", asyncWaits, AtomicInteger::doubleValue) + .description("Pending asynchronous wait operations") + .baseUnit(BaseUnits.OPERATIONS) + .tags(tags) + .register(registry); + } + + @Override + public void handle(String path, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + Timer.Sample sample = Timer.start(registry); + LongTaskTimer.Sample requestSample; + + HttpChannelState state = baseRequest.getHttpChannelState(); + if (state.isInitial()) { + requestSample = openRequests.start(); + request.setAttribute(SAMPLE_REQUEST_TIMER_ATTRIBUTE, sample); + request.setAttribute(SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE, requestSample); + } else { + asyncDispatches.increment(); + request.setAttribute(SAMPLE_REQUEST_TIMER_ATTRIBUTE, sample); + requestSample = (LongTaskTimer.Sample) request.getAttribute(SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE); + } + + try { + Handler handler = getHandler(); + if (handler != null && !shutdown.isShutdown() && isStarted()) { + handler.handle(path, baseRequest, request, response); + } else { + if (!baseRequest.isHandled()) { + baseRequest.setHandled(true); + } + if (!baseRequest.getResponse().isCommitted()) { + response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); + } + } + } finally { + if (state.isSuspended()) { + if (state.isInitial()) { + state.addListener(onCompletion); + asyncWaits.incrementAndGet(); + } + } else if (state.isInitial()) { + sample.stop(Timer.builder("jetty.server.requests") + .description("HTTP requests to the Jetty server") + .tags(tagsProvider.getTags(request, response)) + .tags(tags) + .register(registry)); + + requestSample.stop(); + + // If we have no more dispatches, should we signal shutdown? + FutureCallback shutdownCallback = shutdown.get(); + if (shutdownCallback != null) { + response.flushBuffer(); + if (openRequests.activeTasks() == 0) { + shutdownCallback.succeeded(); + } + } + } + // else onCompletion will handle it. + } + } + + private final AsyncListener onCompletion = new OnCompletionAsyncListener(this); + + @Override + protected void doStart() throws Exception { + shutdown.cancel(); + super.doStart(); + } + + @Override + protected void doStop() throws Exception { + shutdown.cancel(); + super.doStop(); + } + + @Override + public Future shutdown() { + return shutdown.shutdown(); + } + + @Override + public boolean isShutdown() { + return shutdown.isShutdown(); + } + + void onAsyncTimeout(AsyncEvent event) { + asyncExpires.increment(); + + HttpChannelState state = ((AsyncContextEvent) event).getHttpChannelState(); + Request request = state.getBaseRequest(); + + LongTaskTimer.Sample lttSample = (LongTaskTimer.Sample) request.getAttribute(SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE); + lttSample.stop(); + } + + void onAsyncComplete(AsyncEvent event) { + HttpChannelState state = ((AsyncContextEvent) event).getHttpChannelState(); + + Request request = state.getBaseRequest(); + Timer.Sample sample = (Timer.Sample) request.getAttribute(SAMPLE_REQUEST_TIMER_ATTRIBUTE); + LongTaskTimer.Sample lttSample = (LongTaskTimer.Sample) request.getAttribute(SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE); + + if (sample != null) { + sample.stop(Timer.builder("jetty.server.requests") + .description("HTTP requests to the Jetty server") + .tags(tagsProvider.getTags(request, request.getResponse())) + .tags(tags) + .register(registry)); + + lttSample.stop(); + } + + asyncWaits.decrementAndGet(); + + // If we have no more dispatches, should we signal shutdown? + FutureCallback shutdownCallback = shutdown.get(); + if (shutdownCallback != null && openRequests.activeTasks() == 0) { + shutdownCallback.succeeded(); + } + } + + private FutureCallback newShutdownCallback() { + return new FutureCallback(openRequests.activeTasks() == 0); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ClassLoaderMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ClassLoaderMetrics.java new file mode 100644 index 0000000000..3f6f9f068f --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ClassLoaderMetrics.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.ClassLoadingMXBean; +import java.lang.management.ManagementFactory; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Collections.emptyList; + +@NonNullApi +@NonNullFields +public class ClassLoaderMetrics implements MeterBinder { + private final Iterable tags; + + public ClassLoaderMetrics() { + this(emptyList()); + } + + public ClassLoaderMetrics(Iterable tags) { + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + ClassLoadingMXBean classLoadingBean = ManagementFactory.getClassLoadingMXBean(); + + Gauge.builder("jvm.classes.loaded", classLoadingBean, ClassLoadingMXBean::getLoadedClassCount) + .tags(tags) + .description("The number of classes that are currently loaded in the Java virtual machine") + .baseUnit(BaseUnits.CLASSES) + .register(registry); + + FunctionCounter.builder("jvm.classes.unloaded", classLoadingBean, ClassLoadingMXBean::getUnloadedClassCount) + .tags(tags) + .description("The total number of classes unloaded since the Java virtual machine has started execution") + .baseUnit(BaseUnits.CLASSES) + .register(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ExecutorServiceMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ExecutorServiceMetrics.java new file mode 100644 index 0000000000..443344587b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/ExecutorServiceMetrics.java @@ -0,0 +1,410 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.reflect.Field; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import io.micrometer.core.instrument.FunctionCounter; +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.internal.TimedExecutor; +import io.micrometer.core.instrument.internal.TimedExecutorService; +import io.micrometer.core.instrument.internal.TimedScheduledExecutorService; +import io.micrometer.core.instrument.util.StringUtils; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import io.micrometer.core.util.internal.logging.InternalLogger; +import io.micrometer.core.util.internal.logging.InternalLoggerFactory; + +import static java.util.Arrays.asList; + +/** + * Monitors the status of executor service pools. Does not record timings on operations executed in the {@link ExecutorService}, + * as this requires the instance to be wrapped. Timings are provided separately by wrapping the executor service + * with {@link TimedExecutorService}. + *

+ * Supports {@link ThreadPoolExecutor} and {@link ForkJoinPool} types of {@link ExecutorService}. Some libraries may provide + * a wrapper type for {@link ExecutorService}, like {@link TimedExecutorService}. Make sure to pass the underlying, + * unwrapped ExecutorService to this MeterBinder, if it is wrapped in another type. + * + * @author Jon Schneider + * @author Clint Checketts + * @author Johnny Lim + */ +@NonNullApi +@NonNullFields +public class ExecutorServiceMetrics implements MeterBinder { + private static boolean allowIllegalReflectiveAccess = true; + + private static final InternalLogger log = InternalLoggerFactory.getInstance(ExecutorServiceMetrics.class); + private static final String DEFAULT_EXECUTOR_METRIC_PREFIX = ""; + @Nullable + private final ExecutorService executorService; + + private final Iterable tags; + private final String metricPrefix; + + public ExecutorServiceMetrics(@Nullable ExecutorService executorService, String executorServiceName, Iterable tags) { + this(executorService, executorServiceName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Create an {@code ExecutorServiceMetrics} instance. + * + * @param executorService executor service + * @param executorServiceName executor service name which will be used as {@literal name} tag + * @param metricPrefix metrics prefix which will be used to prefix metric name + * @param tags additional tags + * @since 1.5.0 + */ + public ExecutorServiceMetrics(@Nullable ExecutorService executorService, String executorServiceName, + String metricPrefix, Iterable tags) { + this.executorService = executorService; + this.tags = Tags.concat(tags, "name", executorServiceName); + this.metricPrefix = sanitizePrefix(metricPrefix); + } + + /** + * Record metrics on the use of an {@link Executor}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + */ + public static Executor monitor(MeterRegistry registry, Executor executor, String executorName, Iterable tags) { + return monitor(registry, executor, executorName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Record metrics on the use of an {@link Executor}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + * @since 1.5.0 + */ + public static Executor monitor(MeterRegistry registry, Executor executor, String executorName, + String metricPrefix, Iterable tags) { + if (executor instanceof ExecutorService) { + return monitor(registry, (ExecutorService) executor, executorName, metricPrefix, tags); + } + return new TimedExecutor(registry, executor, executorName, sanitizePrefix(metricPrefix), tags); + } + + /** + * Record metrics on the use of an {@link Executor}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + */ + public static Executor monitor(MeterRegistry registry, Executor executor, String executorName, Tag... tags) { + return monitor(registry, executor, executorName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Record metrics on the use of an {@link Executor}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + * @since 1.5.0 + */ + public static Executor monitor(MeterRegistry registry, Executor executor, String executorName, + String metricPrefix, Tag... tags) { + return monitor(registry, executor, executorName, metricPrefix, asList(tags)); + } + + /** + * Record metrics on the use of an {@link ExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + */ + public static ExecutorService monitor(MeterRegistry registry, ExecutorService executor, String executorServiceName, Iterable tags) { + return monitor(registry, executor, executorServiceName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Record metrics on the use of an {@link ExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + * @since 1.5.0 + */ + public static ExecutorService monitor(MeterRegistry registry, ExecutorService executor, String executorServiceName, + String metricPrefix, Iterable tags) { + if (executor instanceof ScheduledExecutorService) { + return monitor(registry, (ScheduledExecutorService) executor, executorServiceName, metricPrefix, tags); + } + new ExecutorServiceMetrics(executor, executorServiceName, metricPrefix, tags).bindTo(registry); + return new TimedExecutorService(registry, executor, executorServiceName, sanitizePrefix(metricPrefix), tags); + } + + /** + * Record metrics on the use of an {@link ExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented executor, proxied. + */ + public static ExecutorService monitor(MeterRegistry registry, ExecutorService executor, String executorServiceName, Tag... tags) { + return monitor(registry, executor, executorServiceName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Record metrics on the use of an {@link ExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @since 1.5.0 + * @return The instrumented executor, proxied. + */ + public static ExecutorService monitor(MeterRegistry registry, ExecutorService executor, String executorServiceName, + String metricPrefix, Tag... tags) { + return monitor(registry, executor, executorServiceName, metricPrefix, asList(tags)); + } + + /** + * Record metrics on the use of a {@link ScheduledExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The scheduled executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented scheduled executor, proxied. + * @since 1.3.0 + */ + public static ScheduledExecutorService monitor(MeterRegistry registry, ScheduledExecutorService executor, String executorServiceName, Iterable tags) { + return monitor(registry, executor, executorServiceName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + + /** + * Record metrics on the use of a {@link ScheduledExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The scheduled executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented scheduled executor, proxied. + * @since 1.5.0 + */ + public static ScheduledExecutorService monitor(MeterRegistry registry, ScheduledExecutorService executor, String executorServiceName, + String metricPrefix, Iterable tags) { + new ExecutorServiceMetrics(executor, executorServiceName, metricPrefix, tags).bindTo(registry); + return new TimedScheduledExecutorService(registry, executor, executorServiceName, sanitizePrefix(metricPrefix), tags); + } + + /** + * Record metrics on the use of a {@link ScheduledExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The scheduled executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented scheduled executor, proxied. + * @since 1.3.0 + */ + public static ScheduledExecutorService monitor(MeterRegistry registry, ScheduledExecutorService executor, String executorServiceName, Tag... tags) { + return monitor(registry, executor, executorServiceName, DEFAULT_EXECUTOR_METRIC_PREFIX, tags); + } + /** + * Record metrics on the use of a {@link ScheduledExecutorService}. + * + * @param registry The registry to bind metrics to. + * @param executor The scheduled executor to instrument. + * @param executorServiceName Will be used to tag metrics with "name". + * @param metricPrefix The prefix to use with meter names. This differentiates executor metrics that may have different tag sets. + * @param tags Tags to apply to all recorded metrics. + * @return The instrumented scheduled executor, proxied. + * @since 1.5.0 + */ + public static ScheduledExecutorService monitor(MeterRegistry registry, ScheduledExecutorService executor, String executorServiceName, + String metricPrefix, Tag... tags) { + return monitor(registry, executor, executorServiceName, metricPrefix, asList(tags)); + } + + private static String sanitizePrefix(String metricPrefix) { + if (StringUtils.isBlank(metricPrefix)) + return ""; + if (!metricPrefix.endsWith(".")) + return metricPrefix + "."; + return metricPrefix; + } + + @Override + public void bindTo(MeterRegistry registry) { + if (executorService == null) { + return; + } + + String className = executorService.getClass().getName(); + + if (executorService instanceof ThreadPoolExecutor) { + monitor(registry, (ThreadPoolExecutor) executorService); + } else if (executorService instanceof ForkJoinPool) { + monitor(registry, (ForkJoinPool) executorService); + } else if (allowIllegalReflectiveAccess) { + if (className.equals("java.util.concurrent.Executors$DelegatedScheduledExecutorService")) { + monitor(registry, unwrapThreadPoolExecutor(executorService, executorService.getClass())); + } else if (className.equals("java.util.concurrent.Executors$FinalizableDelegatedExecutorService")) { + monitor(registry, unwrapThreadPoolExecutor(executorService, executorService.getClass().getSuperclass())); + } else { + log.warn("Failed to bind as {} is unsupported.", className); + } + } else { + log.warn("Failed to bind as {} is unsupported or reflective access is not allowed.", className); + } + } + + /** + * Every ScheduledThreadPoolExecutor created by {@link Executors} is wrapped. Also, + * {@link Executors#newSingleThreadExecutor()} wrap a regular {@link ThreadPoolExecutor}. + */ + @Nullable + private ThreadPoolExecutor unwrapThreadPoolExecutor(ExecutorService executor, Class wrapper) { + try { + Field e = wrapper.getDeclaredField("e"); + e.setAccessible(true); + return (ThreadPoolExecutor) e.get(executor); + } catch (NoSuchFieldException | IllegalAccessException | RuntimeException e) { + // Cannot use InaccessibleObjectException since it was introduced in Java 9, so catch all RuntimeExceptions instead + // Do nothing. We simply can't get to the underlying ThreadPoolExecutor. + log.info("Cannot unwrap ThreadPoolExecutor for monitoring from {} due to {}: {}", wrapper.getName(), e.getClass().getName(), e.getMessage()); + } + return null; + } + + private void monitor(MeterRegistry registry, @Nullable ThreadPoolExecutor tp) { + if (tp == null) { + return; + } + + FunctionCounter.builder(metricPrefix + "executor.completed", tp, ThreadPoolExecutor::getCompletedTaskCount) + .tags(tags) + .description("The approximate total number of tasks that have completed execution") + .baseUnit(BaseUnits.TASKS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.active", tp, ThreadPoolExecutor::getActiveCount) + .tags(tags) + .description("The approximate number of threads that are actively executing tasks") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.queued", tp, tpRef -> tpRef.getQueue().size()) + .tags(tags) + .description("The approximate number of tasks that are queued for execution") + .baseUnit(BaseUnits.TASKS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.queue.remaining", tp, tpRef -> tpRef.getQueue().remainingCapacity()) + .tags(tags) + .description("The number of additional elements that this queue can ideally accept without blocking") + .baseUnit(BaseUnits.TASKS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.pool.size", tp, ThreadPoolExecutor::getPoolSize) + .tags(tags) + .description("The current number of threads in the pool") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.pool.core", tp, ThreadPoolExecutor::getCorePoolSize) + .tags(tags) + .description("The core number of threads for the pool") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder(metricPrefix + "executor.pool.max", tp, ThreadPoolExecutor::getMaximumPoolSize) + .tags(tags) + .description("The maximum allowed number of threads in the pool") + .baseUnit(BaseUnits.THREADS) + .register(registry); + } + + private void monitor(MeterRegistry registry, ForkJoinPool fj) { + FunctionCounter.builder(metricPrefix + "executor.steals", fj, ForkJoinPool::getStealCount) + .tags(tags) + .description("Estimate of the total number of tasks stolen from " + + "one thread's work queue by another. The reported value " + + "underestimates the actual total number of steals when the pool " + + "is not quiescent") + .register(registry); + + Gauge.builder(metricPrefix + "executor.queued", fj, ForkJoinPool::getQueuedTaskCount) + .tags(tags) + .description("An estimate of the total number of tasks currently held in queues by worker threads") + .register(registry); + + Gauge.builder(metricPrefix + "executor.active", fj, ForkJoinPool::getActiveThreadCount) + .tags(tags) + .description("An estimate of the number of threads that are currently stealing or executing tasks") + .register(registry); + + Gauge.builder(metricPrefix + "executor.running", fj, ForkJoinPool::getRunningThreadCount) + .tags(tags) + .description("An estimate of the number of worker threads that are not blocked waiting to join tasks or for other managed synchronization threads") + .register(registry); + } + + /** + * Disable illegal reflective accesses. + * + * Java 9+ warns illegal reflective accesses, but some metrics from this binder depend on reflective access to + * {@link Executors}'s internal implementation details. This method allows to disable the feature to avoid the + * warnings. + * @since 1.6.0 + */ + public static void disableIllegalReflectiveAccess() { + allowIllegalReflectiveAccess = false; + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmCompilationMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmCompilationMetrics.java new file mode 100644 index 0000000000..8ffad4650f --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmCompilationMetrics.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.CompilationMXBean; +import java.lang.management.ManagementFactory; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Collections.emptyList; + +/** + * {@link MeterBinder} for JVM compilation metrics. + * + * @since 1.4.0 + */ +@NonNullApi +@NonNullFields +public class JvmCompilationMetrics implements MeterBinder { + private final Iterable tags; + + public JvmCompilationMetrics() { + this(emptyList()); + } + + public JvmCompilationMetrics(Iterable tags) { + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + CompilationMXBean compilationBean = ManagementFactory.getCompilationMXBean(); + if (compilationBean != null && compilationBean.isCompilationTimeMonitoringSupported()) { + FunctionCounter.builder("jvm.compilation.time", compilationBean, CompilationMXBean::getTotalCompilationTime) + .tags(Tags.concat(tags, "compiler", compilationBean.getName())) + .description("The approximate accumulated elapsed time spent in compilation") + .baseUnit(BaseUnits.MILLISECONDS) + .register(registry); + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmGcMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmGcMetrics.java new file mode 100644 index 0000000000..e813dae2cb --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmGcMetrics.java @@ -0,0 +1,313 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; + +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import io.micrometer.core.util.internal.logging.InternalLogger; +import io.micrometer.core.util.internal.logging.InternalLoggerFactory; + +import static io.micrometer.binder.jvm.JvmMemory.getLongLivedHeapPools; +import static io.micrometer.binder.jvm.JvmMemory.getUsageValue; +import static io.micrometer.binder.jvm.JvmMemory.isAllocationPool; +import static io.micrometer.binder.jvm.JvmMemory.isConcurrentPhase; +import static io.micrometer.binder.jvm.JvmMemory.isLongLivedPool; +import static java.util.Collections.emptyList; + +/** + * Record metrics that report a number of statistics related to garbage + * collection emanating from the MXBean and also adds information about GC causes. + *

+ * This provides metrics for OpenJDK garbage collectors (serial, parallel, G1, Shenandoah, ZGC) + * and for OpenJ9 garbage collectors (gencon, balanced, opthruput, optavgpause, metronome). + * + * @author Jon Schneider + * @author Tommy Ludwig + * @see GarbageCollectorMXBean + */ +@NonNullApi +@NonNullFields +public class JvmGcMetrics implements MeterBinder, AutoCloseable { + + private static final InternalLogger log = InternalLoggerFactory.getInstance(JvmGcMetrics.class); + + private final boolean managementExtensionsPresent = isManagementExtensionsPresent(); + // VisibleForTesting + final boolean isGenerationalGc = isGenerationalGcConfigured(); + + private final Iterable tags; + + @Nullable + private String allocationPoolName; + + private final Set longLivedPoolNames = new HashSet<>(); + + private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>(); + + private Counter allocatedBytes; + @Nullable + private Counter promotedBytes; + private AtomicLong allocationPoolSizeAfter; + private AtomicLong liveDataSize; + private AtomicLong maxDataSize; + + public JvmGcMetrics() { + this(emptyList()); + } + + public JvmGcMetrics(Iterable tags) { + for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) { + String name = mbean.getName(); + if (isAllocationPool(name)) { + allocationPoolName = name; + } + if (isLongLivedPool(name)) { + longLivedPoolNames.add(name); + } + } + this.tags = tags; + } + + // VisibleForTesting + GcMetricsNotificationListener gcNotificationListener; + + @Override + public void bindTo(MeterRegistry registry) { + if (!this.managementExtensionsPresent) { + return; + } + + gcNotificationListener = new GcMetricsNotificationListener(registry); + + double maxLongLivedPoolBytes = getLongLivedHeapPools().mapToDouble(mem -> getUsageValue(mem, MemoryUsage::getMax)).sum(); + + maxDataSize = new AtomicLong((long) maxLongLivedPoolBytes); + Gauge.builder("jvm.gc.max.data.size", maxDataSize, AtomicLong::get) + .tags(tags) + .description("Max size of long-lived heap memory pool") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + liveDataSize = new AtomicLong(); + + Gauge.builder("jvm.gc.live.data.size", liveDataSize, AtomicLong::get) + .tags(tags) + .description("Size of long-lived heap memory pool after reclamation") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + allocatedBytes = Counter.builder("jvm.gc.memory.allocated").tags(tags) + .baseUnit(BaseUnits.BYTES) + .description("Incremented for an increase in the size of the (young) heap memory pool after one GC to before the next") + .register(registry); + + promotedBytes = (isGenerationalGc) ? Counter.builder("jvm.gc.memory.promoted").tags(tags) + .baseUnit(BaseUnits.BYTES) + .description("Count of positive increases in the size of the old generation memory pool before GC to after GC") + .register(registry) : null; + + allocationPoolSizeAfter = new AtomicLong(0L); + + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (!(gcBean instanceof NotificationEmitter)) { + continue; + } + NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; + notificationEmitter.addNotificationListener(gcNotificationListener, notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), null); + notificationListenerCleanUpRunnables.add(() -> { + try { + notificationEmitter.removeNotificationListener(gcNotificationListener); + } catch (ListenerNotFoundException ignore) { + } + }); + } + } + + class GcMetricsNotificationListener implements NotificationListener { + private final MeterRegistry registry; + + GcMetricsNotificationListener(MeterRegistry registry) { + this.registry = registry; + } + + @Override + public void handleNotification(Notification notification, Object ref) { + CompositeData cd = (CompositeData) notification.getUserData(); + GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd); + + String gcCause = notificationInfo.getGcCause(); + String gcAction = notificationInfo.getGcAction(); + GcInfo gcInfo = notificationInfo.getGcInfo(); + long duration = gcInfo.getDuration(); + if (isConcurrentPhase(gcCause, notificationInfo.getGcName())) { + Timer.builder("jvm.gc.concurrent.phase.time") + .tags(tags) + .tags("action", gcAction, "cause", gcCause) + .description("Time spent in concurrent phase") + .register(registry) + .record(duration, TimeUnit.MILLISECONDS); + } else { + Timer.builder("jvm.gc.pause") + .tags(tags) + .tags("action", gcAction, "cause", gcCause) + .description("Time spent in GC pause") + .register(registry) + .record(duration, TimeUnit.MILLISECONDS); + } + + final Map before = gcInfo.getMemoryUsageBeforeGc(); + final Map after = gcInfo.getMemoryUsageAfterGc(); + + countPoolSizeDelta(before, after); + + final long longLivedBefore = longLivedPoolNames.stream().mapToLong(pool -> before.get(pool).getUsed()).sum(); + final long longLivedAfter = longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getUsed()).sum(); + if (isGenerationalGc) { + final long delta = longLivedAfter - longLivedBefore; + if (delta > 0L) { + promotedBytes.increment(delta); + } + } + + // Some GC implementations such as G1 can reduce the old gen size as part of a minor GC. To track the + // live data size we record the value if we see a reduction in the long-lived heap size or + // after a major/non-generational GC. + if (longLivedAfter < longLivedBefore || shouldUpdateDataSizeMetrics(notificationInfo.getGcName())) { + liveDataSize.set(longLivedAfter); + maxDataSize.set(longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getMax()).sum()); + } + } + + private void countPoolSizeDelta(Map before, Map after) { + if (allocationPoolName == null) { + return; + } + final long beforeBytes = before.get(allocationPoolName).getUsed(); + final long afterBytes = after.get(allocationPoolName).getUsed(); + final long delta = beforeBytes - allocationPoolSizeAfter.get(); + allocationPoolSizeAfter.set(afterBytes); + if (delta > 0L) { + allocatedBytes.increment(delta); + } + } + + private boolean shouldUpdateDataSizeMetrics(String gcName) { + return nonGenerationalGcShouldUpdateDataSize(gcName) || isMajorGenerationalGc(gcName); + } + + private boolean isMajorGenerationalGc(String gcName) { + return GcGenerationAge.fromGcName(gcName) == GcGenerationAge.OLD; + } + + private boolean nonGenerationalGcShouldUpdateDataSize(String gcName) { + return !isGenerationalGc + // Skip Shenandoah and ZGC gc notifications with the name Pauses due to missing memory pool size info + && !gcName.endsWith("Pauses"); + } + } + + private boolean isGenerationalGcConfigured() { + return ManagementFactory.getMemoryPoolMXBeans().stream() + .filter(JvmMemory::isHeap) + .map(MemoryPoolMXBean::getName) + .filter(name -> !name.contains("tenured")) + .count() > 1; + } + + private static boolean isManagementExtensionsPresent() { + if ( ManagementFactory.getMemoryPoolMXBeans().isEmpty() ) { + // Substrate VM, for example, doesn't provide or support these beans (yet) + log.warn("GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM"); + return false; + } + + try { + Class.forName("com.sun.management.GarbageCollectionNotificationInfo", false, + MemoryPoolMXBean.class.getClassLoader()); + return true; + } catch (Throwable e) { + // We are operating in a JVM without access to this level of detail + log.warn("GC notifications will not be available because " + + "com.sun.management.GarbageCollectionNotificationInfo is not present"); + return false; + } + } + + @Override + public void close() { + notificationListenerCleanUpRunnables.forEach(Runnable::run); + } + + /** + * Generalization of which parts of the heap are considered "young" or "old" for multiple GC implementations + */ + @NonNullApi + enum GcGenerationAge { + OLD, + YOUNG, + UNKNOWN; + + private static final Map knownCollectors = new HashMap() {{ + put("ConcurrentMarkSweep", OLD); + put("Copy", YOUNG); + put("G1 Old Generation", OLD); + put("G1 Young Generation", YOUNG); + put("MarkSweepCompact", OLD); + put("PS MarkSweep", OLD); + put("PS Scavenge", YOUNG); + put("ParNew", YOUNG); + put("global", OLD); + put("scavenge", YOUNG); + put("partial gc", YOUNG); + put("global garbage collect", OLD); + put("Epsilon", OLD); + }}; + + static GcGenerationAge fromGcName(String gcName) { + return knownCollectors.getOrDefault(gcName, UNKNOWN); + } + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmHeapPressureMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmHeapPressureMetrics.java new file mode 100644 index 0000000000..8ac7bbdd47 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmHeapPressureMetrics.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 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.binder.jvm; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryUsage; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import javax.management.ListenerNotFoundException; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import javax.management.openmbean.CompositeData; + +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.distribution.TimeWindowSum; +import io.micrometer.core.lang.NonNull; + +import static java.util.Collections.emptyList; + +/** + * Provides methods to access measurements of low pool memory and heavy GC overhead as described in + * TeamCity's Memory Monitor. + * + * @author Jon Schneider + * @since 1.4.0 + */ +public class JvmHeapPressureMetrics implements MeterBinder, AutoCloseable { + private final Iterable tags; + + private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>(); + + private final long startOfMonitoring = System.nanoTime(); + private final Duration lookback; + private final TimeWindowSum gcPauseSum; + private final AtomicReference lastLongLivedPoolUsageAfterGc = new AtomicReference<>(0.0); + + private final Set longLivedPoolNames; + + public JvmHeapPressureMetrics() { + this(emptyList(), Duration.ofMinutes(5), Duration.ofMinutes(1)); + } + + public JvmHeapPressureMetrics(Iterable tags, Duration lookback, Duration testEvery) { + this.tags = tags; + this.lookback = lookback; + this.gcPauseSum = new TimeWindowSum((int) lookback.dividedBy(testEvery.toMillis()).toMillis(), testEvery); + + longLivedPoolNames = JvmMemory.getLongLivedHeapPools().map(MemoryPoolMXBean::getName).collect(Collectors.toSet()); + + monitor(); + } + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + if (!longLivedPoolNames.isEmpty()) { + Gauge.builder("jvm.memory.usage.after.gc", lastLongLivedPoolUsageAfterGc, AtomicReference::get) + .tags(tags) + .tag("area", "heap") + .tag("pool", "long-lived") + .description("The percentage of long-lived heap pool used after the last GC event, in the range [0..1]") + .baseUnit(BaseUnits.PERCENT) + .register(registry); + } + + Gauge.builder("jvm.gc.overhead", gcPauseSum, + pauseSum -> { + double overIntervalMillis = Math.min(System.nanoTime() - startOfMonitoring, lookback.toNanos()) / 1e6; + return gcPauseSum.poll() / overIntervalMillis; + }) + .tags(tags) + .description("An approximation of the percent of CPU time used by GC activities over the last lookback period or since monitoring began, whichever is shorter, in the range [0..1]") + .baseUnit(BaseUnits.PERCENT) + .register(registry); + } + + private void monitor() { + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (!(mbean instanceof NotificationEmitter)) { + continue; + } + NotificationListener notificationListener = (notification, ref) -> { + CompositeData cd = (CompositeData) notification.getUserData(); + GarbageCollectionNotificationInfo notificationInfo = GarbageCollectionNotificationInfo.from(cd); + + String gcCause = notificationInfo.getGcCause(); + GcInfo gcInfo = notificationInfo.getGcInfo(); + long duration = gcInfo.getDuration(); + + if (!JvmMemory.isConcurrentPhase(gcCause, notificationInfo.getGcName())) { + gcPauseSum.record(duration); + } + + Map after = gcInfo.getMemoryUsageAfterGc(); + + if (!longLivedPoolNames.isEmpty()) { + final long usedAfter = longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getUsed()).sum(); + double maxAfter = longLivedPoolNames.stream().mapToLong(pool -> after.get(pool).getMax()).sum(); + lastLongLivedPoolUsageAfterGc.set(usedAfter / maxAfter); + } + }; + NotificationEmitter notificationEmitter = (NotificationEmitter) mbean; + notificationEmitter.addNotificationListener(notificationListener, + notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), + null); + notificationListenerCleanUpRunnables.add(() -> { + try { + notificationEmitter.removeNotificationListener(notificationListener); + } catch (ListenerNotFoundException ignore) { + } + }); + } + } + + @Override + public void close() { + notificationListenerCleanUpRunnables.forEach(Runnable::run); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmInfoMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmInfoMetrics.java new file mode 100644 index 0000000000..d3785ca3c0 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmInfoMetrics.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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.binder.jvm; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; + +/** + * {@link MeterBinder} for JVM information. + * + * @author Erin Schnabel + * @since 1.7.0 + */ +public class JvmInfoMetrics implements MeterBinder { + + @Override + public void bindTo(MeterRegistry registry) { + Gauge.builder("jvm.info", () -> 1L) + .description("JVM version info") + .tags("version", System.getProperty("java.runtime.version", "unknown"), + "vendor", System.getProperty("java.vm.vendor", "unknown"), + "runtime", System.getProperty("java.runtime.name", "unknown")) + .strongReference(true) + .register(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemory.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemory.java new file mode 100644 index 0000000000..3341e3d41b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 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.binder.jvm; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; +import java.util.function.ToLongFunction; +import java.util.stream.Stream; + +import io.micrometer.core.lang.Nullable; + +class JvmMemory { + + private JvmMemory() { + } + + static Stream getLongLivedHeapPools() { + return ManagementFactory + .getMemoryPoolMXBeans() + .stream() + .filter(JvmMemory::isHeap) + .filter(mem -> isLongLivedPool(mem.getName())); + } + + static boolean isConcurrentPhase(String cause, String name) { + return "No GC".equals(cause) + || "Shenandoah Cycles".equals(name) + || "ZGC Cycles".equals(name); + } + + static boolean isAllocationPool(String name) { + return name != null && (name.endsWith("Eden Space") + || "Shenandoah".equals(name) + || "ZHeap".equals(name) + || name.endsWith("nursery-allocate") + || name.endsWith("-eden") // "balanced-eden" + || "JavaHeap".equals(name) // metronome + ); + } + + static boolean isLongLivedPool(String name) { + return name != null && (name.endsWith("Old Gen") + || name.endsWith("Tenured Gen") + || "Shenandoah".equals(name) + || "ZHeap".equals(name) + || name.endsWith("balanced-old") + || name.contains("tenured") // "tenured", "tenured-SOA", "tenured-LOA" + || "JavaHeap".equals(name) // metronome + ); + } + + static boolean isHeap(MemoryPoolMXBean memoryPoolBean) { + return MemoryType.HEAP.equals(memoryPoolBean.getType()); + } + + static double getUsageValue(MemoryPoolMXBean memoryPoolMXBean, ToLongFunction getter) { + MemoryUsage usage = getUsage(memoryPoolMXBean); + if (usage == null) { + return Double.NaN; + } + return getter.applyAsLong(usage); + } + + @Nullable + private static MemoryUsage getUsage(MemoryPoolMXBean memoryPoolMXBean) { + try { + return memoryPoolMXBean.getUsage(); + } catch (InternalError e) { + // Defensive for potential InternalError with some specific JVM options. Based on its Javadoc, + // MemoryPoolMXBean.getUsage() should return null, not throwing InternalError, so it seems to be a JVM bug. + return null; + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemoryMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemoryMetrics.java new file mode 100644 index 0000000000..b3f97474de --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmMemoryMetrics.java @@ -0,0 +1,105 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.BufferPoolMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.lang.management.MemoryType; +import java.lang.management.MemoryUsage; + +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static io.micrometer.binder.jvm.JvmMemory.getUsageValue; +import static java.util.Collections.emptyList; + +/** + * Record metrics that report utilization of various memory and buffer pools. + * + * @author Jon Schneider + * @author Johnny Lim + * @see MemoryPoolMXBean + * @see BufferPoolMXBean + */ +@NonNullApi +@NonNullFields +public class JvmMemoryMetrics implements MeterBinder { + private final Iterable tags; + + public JvmMemoryMetrics() { + this(emptyList()); + } + + public JvmMemoryMetrics(Iterable tags) { + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + for (BufferPoolMXBean bufferPoolBean : ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class)) { + Iterable tagsWithId = Tags.concat(tags, "id", bufferPoolBean.getName()); + + Gauge.builder("jvm.buffer.count", bufferPoolBean, BufferPoolMXBean::getCount) + .tags(tagsWithId) + .description("An estimate of the number of buffers in the pool") + .baseUnit(BaseUnits.BUFFERS) + .register(registry); + + Gauge.builder("jvm.buffer.memory.used", bufferPoolBean, BufferPoolMXBean::getMemoryUsed) + .tags(tagsWithId) + .description("An estimate of the memory that the Java virtual machine is using for this buffer pool") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("jvm.buffer.total.capacity", bufferPoolBean, BufferPoolMXBean::getTotalCapacity) + .tags(tagsWithId) + .description("An estimate of the total capacity of the buffers in this pool") + .baseUnit(BaseUnits.BYTES) + .register(registry); + } + + for (MemoryPoolMXBean memoryPoolBean : ManagementFactory.getPlatformMXBeans(MemoryPoolMXBean.class)) { + String area = MemoryType.HEAP.equals(memoryPoolBean.getType()) ? "heap" : "nonheap"; + Iterable tagsWithId = Tags.concat(tags, "id", memoryPoolBean.getName(), "area", area); + + Gauge.builder("jvm.memory.used", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getUsed)) + .tags(tagsWithId) + .description("The amount of used memory") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("jvm.memory.committed", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getCommitted)) + .tags(tagsWithId) + .description("The amount of memory in bytes that is committed for the Java virtual machine to use") + .baseUnit(BaseUnits.BYTES) + .register(registry); + + Gauge.builder("jvm.memory.max", memoryPoolBean, (mem) -> getUsageValue(mem, MemoryUsage::getMax)) + .tags(tagsWithId) + .description("The maximum amount of memory in bytes that can be used for memory management") + .baseUnit(BaseUnits.BYTES) + .register(registry); + } + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmThreadMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmThreadMetrics.java new file mode 100644 index 0000000000..450f967c0b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jvm/JvmThreadMetrics.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.Arrays; + +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Collections.emptyList; + +/** + * {@link MeterBinder} for JVM threads. + * + * @author Jon Schneider + * @author Johnny Lim + */ +@NonNullApi +@NonNullFields +public class JvmThreadMetrics implements MeterBinder { + private final Iterable tags; + + public JvmThreadMetrics() { + this(emptyList()); + } + + public JvmThreadMetrics(Iterable tags) { + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + + Gauge.builder("jvm.threads.peak", threadBean, ThreadMXBean::getPeakThreadCount) + .tags(tags) + .description("The peak live thread count since the Java virtual machine started or peak was reset") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder("jvm.threads.daemon", threadBean, ThreadMXBean::getDaemonThreadCount) + .tags(tags) + .description("The current number of live daemon threads") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder("jvm.threads.live", threadBean, ThreadMXBean::getThreadCount) + .tags(tags) + .description("The current number of live threads including both daemon and non-daemon threads") + .baseUnit(BaseUnits.THREADS) + .register(registry); + + try { + threadBean.getAllThreadIds(); + for (Thread.State state : Thread.State.values()) { + Gauge.builder("jvm.threads.states", threadBean, (bean) -> getThreadStateCount(bean, state)) + .tags(Tags.concat(tags, "state", getStateTagValue(state))) + .description("The current number of threads having " + state + " state") + .baseUnit(BaseUnits.THREADS) + .register(registry); + } + } catch (Error error) { + // An error will be thrown for unsupported operations + // e.g. SubstrateVM does not support getAllThreadIds + } + } + + // VisibleForTesting + static long getThreadStateCount(ThreadMXBean threadBean, Thread.State state) { + return Arrays.stream(threadBean.getThreadInfo(threadBean.getAllThreadIds())) + .filter(threadInfo -> threadInfo != null && threadInfo.getThreadState() == state) + .count(); + } + + private static String getStateTagValue(Thread.State state) { + return state.name().toLowerCase().replace("_", "-"); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaClientMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaClientMetrics.java new file mode 100644 index 0000000000..b2c9dd9082 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaClientMetrics.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 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.binder.kafka; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.common.Metric; + +/** + * Kafka Client metrics binder. This should be closed on application shutdown to clean up resources. + *

+ * It is based on the Kafka client's {@code metrics()} method returning a {@link Metric} map. + *

+ * Meter names have the following convention: {@code kafka.(metric_group).(metric_name)} + * + * @author Jorge Quilcate + * @see Kakfa monitoring + * documentation + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +@NonNullApi +@NonNullFields +public class KafkaClientMetrics extends KafkaMetrics { + + /** + * Kafka {@link Producer} metrics binder + * + * @param kafkaProducer producer instance to be instrumented + * @param tags additional tags + */ + public KafkaClientMetrics(Producer kafkaProducer, Iterable tags) { + super(kafkaProducer::metrics, tags); + } + + /** + * Kafka {@link Producer} metrics binder + * + * @param kafkaProducer producer instance to be instrumented + */ + public KafkaClientMetrics(Producer kafkaProducer) { + super(kafkaProducer::metrics); + } + + /** + * Kafka {@link Consumer} metrics binder + * + * @param kafkaConsumer consumer instance to be instrumented + * @param tags additional tags + */ + public KafkaClientMetrics(Consumer kafkaConsumer, Iterable tags) { + super(kafkaConsumer::metrics, tags); + } + + /** + * Kafka {@link Consumer} metrics binder + * + * @param kafkaConsumer consumer instance to be instrumented + */ + public KafkaClientMetrics(Consumer kafkaConsumer) { + super(kafkaConsumer::metrics); + } + + /** + * Kafka {@link AdminClient} metrics binder + * + * @param adminClient instance to be instrumented + * @param tags additional tags + */ + public KafkaClientMetrics(AdminClient adminClient, Iterable tags) { + super(adminClient::metrics, tags); + } + + /** + * Kafka {@link AdminClient} metrics binder + * + * @param adminClient instance to be instrumented + */ + public KafkaClientMetrics(AdminClient adminClient) { + super(adminClient::metrics); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaMetrics.java new file mode 100644 index 0000000000..927c017a00 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaMetrics.java @@ -0,0 +1,309 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +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.micrometer.core.instrument.util.NamedThreadFactory; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import io.micrometer.core.util.internal.logging.InternalLogger; +import io.micrometer.core.util.internal.logging.InternalLoggerFactory; +import io.micrometer.core.util.internal.logging.WarnThenDebugLogger; +import org.apache.kafka.common.Metric; +import org.apache.kafka.common.MetricName; + +import static io.micrometer.core.instrument.Meter.Type.OTHER; +import static java.util.Collections.emptyList; + +/** + * Kafka metrics binder. This should be closed on application shutdown to clean up resources. + * + * @author Jorge Quilcate + * @see Kakfa monitoring + * documentation + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +@NonNullApi +@NonNullFields +class KafkaMetrics implements MeterBinder, AutoCloseable { + private static final InternalLogger log = InternalLoggerFactory.getInstance(KafkaMetrics.class); + private static final WarnThenDebugLogger warnThenDebugLogger = new WarnThenDebugLogger(KafkaMetrics.class); + + static final String METRIC_NAME_PREFIX = "kafka."; + static final String METRIC_GROUP_APP_INFO = "app-info"; + static final String METRIC_GROUP_METRICS_COUNT = "kafka-metrics-count"; + static final String VERSION_METRIC_NAME = "version"; + static final String START_TIME_METRIC_NAME = "start-time-ms"; + static final Duration DEFAULT_REFRESH_INTERVAL = Duration.ofSeconds(60); + static final String KAFKA_VERSION_TAG_NAME = "kafka.version"; + static final String DEFAULT_VALUE = "unknown"; + + private final Supplier> metricsSupplier; + private final AtomicReference> metrics = new AtomicReference<>(); + private final Iterable extraTags; + private final Duration refreshInterval; + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("micrometer-kafka-metrics")); + + @Nullable + private Iterable commonTags; + + /** + * Keeps track of current set of metrics. + */ + private volatile Set currentMeters = new HashSet<>(); + + private String kafkaVersion = DEFAULT_VALUE; + + @Nullable + private volatile MeterRegistry registry; + + private final Set registeredMeterIds = ConcurrentHashMap.newKeySet(); + + KafkaMetrics(Supplier> metricsSupplier) { + this(metricsSupplier, emptyList()); + } + + KafkaMetrics(Supplier> metricsSupplier, Iterable extraTags) { + this(metricsSupplier, extraTags, DEFAULT_REFRESH_INTERVAL); + } + + KafkaMetrics(Supplier> metricsSupplier, Iterable extraTags, Duration refreshInterval) { + this.metricsSupplier = metricsSupplier; + this.extraTags = extraTags; + this.refreshInterval = refreshInterval; + } + + @Override + public void bindTo(MeterRegistry registry) { + this.registry = registry; + + commonTags = getCommonTags(registry); + prepareToBindMetrics(registry); + checkAndBindMetrics(registry); + scheduler.scheduleAtFixedRate(() -> checkAndBindMetrics(registry), getRefreshIntervalInMillis(), getRefreshIntervalInMillis(), TimeUnit.MILLISECONDS); + } + + private Iterable getCommonTags(MeterRegistry registry) { + // FIXME hack until we have proper API to retrieve common tags + Meter.Id dummyId = Meter.builder("delete.this", OTHER, Collections.emptyList()).register(registry).getId(); + registry.remove(dummyId); + return dummyId.getTags(); + } + + /** + * Define common tags and meters before binding metrics + */ + void prepareToBindMetrics(MeterRegistry registry) { + this.metrics.set(this.metricsSupplier.get()); + Map metrics = this.metrics.get(); + // Collect static metrics and tags + MetricName startTime = null; + + for (Map.Entry entry : metrics.entrySet()) { + MetricName name = entry.getKey(); + if (METRIC_GROUP_APP_INFO.equals(name.group())) + if (VERSION_METRIC_NAME.equals(name.name())) { + kafkaVersion = (String) entry.getValue().metricValue(); + } else if (START_TIME_METRIC_NAME.equals(name.name())) { + startTime = entry.getKey(); + } + } + + if (startTime != null) { + bindMeter(registry, startTime, meterName(startTime), meterTags(startTime)); + } + } + + private long getRefreshIntervalInMillis() { + return refreshInterval.toMillis(); + } + + /** + * Gather metrics from Kafka metrics API and register Meters. + *

+ * As this is a one-off execution when binding a Kafka client, Meters include a call to this + * validation to double-check new metrics when returning values. This should only add the cost of + * comparing meters last returned from the Kafka client. + */ + void checkAndBindMetrics(MeterRegistry registry) { + try { + Map currentMetrics = this.metricsSupplier.get(); + this.metrics.set(currentMetrics); + + if (!currentMeters.equals(currentMetrics.keySet())) { + Set metricsToRemove = currentMeters.stream() + .filter(metricName -> !currentMetrics.containsKey(metricName)) + .collect(Collectors.toSet()); + + for (MetricName metricName : metricsToRemove) { + Meter.Id id = meterIdForComparison(metricName); + registry.remove(id); + registeredMeterIds.remove(id); + } + + currentMeters = new HashSet<>(currentMetrics.keySet()); + + Map> registryMetersByNames = registry.getMeters().stream() + .collect(Collectors.groupingBy(meter -> meter.getId().getName())); + + currentMetrics.forEach((name, metric) -> { + // Filter out non-numeric values + // Filter out metrics from groups that include metadata + if (!(metric.metricValue() instanceof Number) || + METRIC_GROUP_APP_INFO.equals(name.group()) || + METRIC_GROUP_METRICS_COUNT.equals(name.group())) { + return; + } + + String meterName = meterName(name); + + // Kafka has metrics with lower number of tags (e.g. with/without topic or partition tag) + // Remove meters with lower number of tags + boolean hasLessTags = false; + for (Meter other : registryMetersByNames.getOrDefault(meterName, emptyList())) { + Meter.Id otherId = other.getId(); + List tags = otherId.getTags(); + List meterTagsWithCommonTags = meterTags(name, true); + if (tags.size() < meterTagsWithCommonTags.size()) { + registry.remove(otherId); + registeredMeterIds.remove(otherId); + } + // Check if already exists + else if (tags.size() == meterTagsWithCommonTags.size()) + if (tags.containsAll(meterTagsWithCommonTags)) return; + else break; + else hasLessTags = true; + } + if (hasLessTags) return; + + List tags = meterTags(name); + try { + Meter meter = bindMeter(registry, metric.metricName(), meterName, tags); + List meters = registryMetersByNames.computeIfAbsent(meterName, k -> new ArrayList<>()); + meters.add(meter); + } + catch (Exception ex) { + String message = ex.getMessage(); + if (message != null && message.contains("Prometheus requires")) { + warnThenDebugLogger.log("Failed to bind meter: " + meterName + " " + tags + + ". However, this could happen and might be restored in the next refresh."); + } + else { + log.warn("Failed to bind meter: " + meterName + " " + tags + ".", ex); + } + } + }); + } + } + catch (Exception e) { + log.warn("Failed to bind KafkaMetric", e); + } + } + + private Meter bindMeter(MeterRegistry registry, MetricName metricName, String meterName, Iterable tags) { + Meter meter = registerMeter(registry, metricName, meterName, tags); + registeredMeterIds.add(meter.getId()); + return meter; + } + + private Meter registerMeter(MeterRegistry registry, MetricName metricName, String meterName, Iterable tags) { + if (meterName.endsWith("total") || meterName.endsWith("count")) { + return registerCounter(registry, metricName, meterName, tags); + } else { + return registerGauge(registry, metricName, meterName, tags); + } + } + + private Gauge registerGauge(MeterRegistry registry, MetricName metricName, String meterName, Iterable tags) { + return Gauge.builder(meterName, this.metrics, toMetricValue(metricName)) + .tags(tags) + .description(metricName.description()) + .register(registry); + } + + private FunctionCounter registerCounter(MeterRegistry registry, MetricName metricName, String meterName, Iterable tags) { + return FunctionCounter.builder(meterName, this.metrics, toMetricValue(metricName)) + .tags(tags) + .description(metricName.description()) + .register(registry); + } + + private ToDoubleFunction>> toMetricValue(MetricName metricName) { + return metricsReference -> toDouble(metricsReference.get().get(metricName)); + } + + private double toDouble(@Nullable Metric metric) { + return (metric != null) ? ((Number) metric.metricValue()).doubleValue() : Double.NaN; + } + + private List meterTags(MetricName metricName, boolean includeCommonTags) { + List tags = new ArrayList<>(); + metricName.tags().forEach((key, value) -> tags.add(Tag.of(key.replaceAll("-", "."), value))); + tags.add(Tag.of(KAFKA_VERSION_TAG_NAME, kafkaVersion)); + extraTags.forEach(tags::add); + if (includeCommonTags) { + commonTags.forEach(tags::add); + } + return tags; + } + + private List meterTags(MetricName metricName) { + return meterTags(metricName, false); + } + + private String meterName(MetricName metricName) { + String name = METRIC_NAME_PREFIX + metricName.group() + "." + metricName.name(); + return name.replaceAll("-metrics", "").replaceAll("-", "."); + } + + private Meter.Id meterIdForComparison(MetricName metricName) { + return new Meter.Id(meterName(metricName), Tags.of(meterTags(metricName, true)), null, null, OTHER); + } + + @Override + public void close() { + this.scheduler.shutdownNow(); + + for (Meter.Id id : registeredMeterIds) { + registry.remove(id); + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaStreamsMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaStreamsMetrics.java new file mode 100644 index 0000000000..0b13000a08 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/kafka/KafkaStreamsMetrics.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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.binder.kafka; + +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import org.apache.kafka.common.Metric; +import org.apache.kafka.streams.KafkaStreams; + +/** + * Kafka Streams metrics binder. This should be closed on application shutdown to clean up resources. + *

+ * It is based on the Kafka client's {@code metrics()} method returning a {@link Metric} map. + *

+ * Meter names have the following convention: {@code kafka.(metric_group).(metric_name)} + * + * @author Jorge Quilcate + * @see Kakfa monitoring + * documentation + * @since 1.4.0 + */ +@Incubating(since = "1.4.0") +@NonNullApi +@NonNullFields +public class KafkaStreamsMetrics extends KafkaMetrics { + + /** + * {@link KafkaStreams} metrics binder + * + * @param kafkaStreams instance to be instrumented + * @param tags additional tags + */ + public KafkaStreamsMetrics(KafkaStreams kafkaStreams, Iterable tags) { + super(kafkaStreams::metrics, tags); + } + + /** + * {@link KafkaStreams} metrics binder + * + * @param kafkaStreams instance to be instrumented + */ + public KafkaStreamsMetrics(KafkaStreams kafkaStreams) { + super(kafkaStreams::metrics); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/logging/Log4j2Metrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/logging/Log4j2Metrics.java new file mode 100644 index 0000000000..34857ca19a --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/logging/Log4j2Metrics.java @@ -0,0 +1,226 @@ +/* + * Copyright 2017 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.binder.logging; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.async.AsyncLoggerConfig; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.core.filter.CompositeFilter; + +import static java.util.Collections.emptyList; + +/** + * {@link MeterBinder} for Apache Log4j 2. + * + * @author Steven Sheehy + * @author Johnny Lim + * @since 1.1.0 + */ +@NonNullApi +@NonNullFields +public class Log4j2Metrics implements MeterBinder, AutoCloseable { + + private static final String METER_NAME = "log4j2.events"; + + private final Iterable tags; + private final LoggerContext loggerContext; + + private List metricsFilters = new ArrayList<>(); + + public Log4j2Metrics() { + this(emptyList()); + } + + public Log4j2Metrics(Iterable tags) { + this(tags, (LoggerContext) LogManager.getContext(false)); + } + + public Log4j2Metrics(Iterable tags, LoggerContext loggerContext) { + this.tags = tags; + this.loggerContext = loggerContext; + } + + @Override + public void bindTo(MeterRegistry registry) { + + Configuration configuration = loggerContext.getConfiguration(); + LoggerConfig rootLoggerConfig = configuration.getRootLogger(); + rootLoggerConfig.addFilter(createMetricsFilterAndStart(registry, rootLoggerConfig)); + + loggerContext.getConfiguration().getLoggers().values().stream() + .filter(loggerConfig -> !loggerConfig.isAdditive()) + .forEach(loggerConfig -> { + if (loggerConfig == rootLoggerConfig) { + return; + } + Filter logFilter = loggerConfig.getFilter(); + + if ((logFilter instanceof CompositeFilter && Arrays.stream(((CompositeFilter) logFilter).getFiltersArray()) + .anyMatch(innerFilter -> innerFilter instanceof MetricsFilter))) { + return; + } + + if (logFilter instanceof MetricsFilter) { + return; + } + loggerConfig.addFilter(createMetricsFilterAndStart(registry, loggerConfig)); + }); + + loggerContext.updateLoggers(configuration); + } + + private MetricsFilter createMetricsFilterAndStart(MeterRegistry registry, LoggerConfig loggerConfig) { + MetricsFilter metricsFilter = new MetricsFilter(registry, tags, loggerConfig instanceof AsyncLoggerConfig); + metricsFilter.start(); + metricsFilters.add(metricsFilter); + return metricsFilter; + } + + @Override + public void close() { + if (!metricsFilters.isEmpty()) { + Configuration configuration = loggerContext.getConfiguration(); + LoggerConfig rootLoggerConfig = configuration.getRootLogger(); + metricsFilters.forEach(rootLoggerConfig::removeFilter); + + loggerContext.getConfiguration().getLoggers().values().stream() + .filter(loggerConfig -> !loggerConfig.isAdditive()) + .forEach(loggerConfig -> { + if (loggerConfig != rootLoggerConfig) { + metricsFilters.forEach(loggerConfig::removeFilter); + } + }); + + loggerContext.updateLoggers(configuration); + metricsFilters.forEach(MetricsFilter::stop); + } + } + + @NonNullApi + @NonNullFields + class MetricsFilter extends AbstractFilter { + + private final Counter fatalCounter; + private final Counter errorCounter; + private final Counter warnCounter; + private final Counter infoCounter; + private final Counter debugCounter; + private final Counter traceCounter; + private final boolean isAsyncLogger; + + MetricsFilter(MeterRegistry registry, Iterable tags, boolean isAsyncLogger) { + this.isAsyncLogger = isAsyncLogger; + fatalCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "fatal") + .description("Number of fatal level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + errorCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "error") + .description("Number of error level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + warnCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "warn") + .description("Number of warn level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + infoCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "info") + .description("Number of info level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + debugCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "debug") + .description("Number of debug level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + traceCounter = Counter.builder(METER_NAME) + .tags(tags) + .tags("level", "trace") + .description("Number of trace level log events") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + } + + @Override + public Result filter(LogEvent event) { + + if (!isAsyncLogger || isAsyncLoggerAndEndOfBatch(event)) { + incrementCounter(event); + } + + return Result.NEUTRAL; + } + + + private boolean isAsyncLoggerAndEndOfBatch(LogEvent event) { + return isAsyncLogger && event.isEndOfBatch(); + } + + private void incrementCounter(LogEvent event) { + switch (event.getLevel().getStandardLevel()) { + case FATAL: + fatalCounter.increment(); + break; + case ERROR: + errorCounter.increment(); + break; + case WARN: + warnCounter.increment(); + break; + case INFO: + infoCounter.increment(); + break; + case DEBUG: + debugCounter.increment(); + break; + case TRACE: + traceCounter.increment(); + break; + default: + break; + } + } + } +} + diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/logging/LogbackMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/logging/LogbackMetrics.java new file mode 100644 index 0000000000..32e6d8ef90 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/logging/LogbackMetrics.java @@ -0,0 +1,208 @@ +/* + * Copyright 2017 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.binder.logging; + +import java.util.HashMap; +import java.util.Map; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggerContextListener; +import ch.qos.logback.classic.turbo.TurboFilter; +import ch.qos.logback.core.spi.FilterReply; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +import static java.util.Collections.emptyList; + +/** + * @author Jon Schneider + */ +@NonNullApi +@NonNullFields +public class LogbackMetrics implements MeterBinder, AutoCloseable { + static ThreadLocal ignoreMetrics = new ThreadLocal<>(); + + private final Iterable tags; + private final LoggerContext loggerContext; + private final Map metricsTurboFilters = new HashMap<>(); + + public LogbackMetrics() { + this(emptyList()); + } + + public LogbackMetrics(Iterable tags) { + this(tags, (LoggerContext) LoggerFactory.getILoggerFactory()); + } + + public LogbackMetrics(Iterable tags, LoggerContext context) { + this.tags = tags; + this.loggerContext = context; + + loggerContext.addListener(new LoggerContextListener() { + @Override + public boolean isResetResistant() { + return true; + } + + @Override + public void onReset(LoggerContext context) { + // re-add turbo filter because reset clears the turbo filter list + synchronized (metricsTurboFilters) { + for (MetricsTurboFilter metricsTurboFilter : metricsTurboFilters.values()) { + loggerContext.addTurboFilter(metricsTurboFilter); + } + } + } + + @Override + public void onStart(LoggerContext context) { + // no-op + } + + @Override + public void onStop(LoggerContext context) { + // no-op + } + + @Override + public void onLevelChange(Logger logger, Level level) { + // no-op + } + }); + } + + @Override + public void bindTo(MeterRegistry registry) { + MetricsTurboFilter filter = new MetricsTurboFilter(registry, tags); + synchronized (metricsTurboFilters) { + metricsTurboFilters.put(registry, filter); + loggerContext.addTurboFilter(filter); + } + } + + /** + * Used by {@link Counter#increment()} implementations that may cause a logback logging event to occur. + * Attempting to instrument that implementation would cause a {@link StackOverflowError}. + * + * @param r Don't record metrics on logging statements that occur inside of this runnable. + */ + public static void ignoreMetrics(Runnable r) { + ignoreMetrics.set(true); + try { + r.run(); + } finally { + ignoreMetrics.remove(); + } + } + + @Override + public void close() { + synchronized (metricsTurboFilters) { + for (MetricsTurboFilter metricsTurboFilter : metricsTurboFilters.values()) { + loggerContext.getTurboFilterList().remove(metricsTurboFilter); + } + } + } +} + +@NonNullApi +@NonNullFields +class MetricsTurboFilter extends TurboFilter { + private final Counter errorCounter; + private final Counter warnCounter; + private final Counter infoCounter; + private final Counter debugCounter; + private final Counter traceCounter; + + MetricsTurboFilter(MeterRegistry registry, Iterable tags) { + errorCounter = Counter.builder("logback.events") + .tags(tags).tags("level", "error") + .description("Number of error level events that made it to the logs") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + warnCounter = Counter.builder("logback.events") + .tags(tags).tags("level", "warn") + .description("Number of warn level events that made it to the logs") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + infoCounter = Counter.builder("logback.events") + .tags(tags).tags("level", "info") + .description("Number of info level events that made it to the logs") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + debugCounter = Counter.builder("logback.events") + .tags(tags).tags("level", "debug") + .description("Number of debug level events that made it to the logs") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + + traceCounter = Counter.builder("logback.events") + .tags(tags).tags("level", "trace") + .description("Number of trace level events that made it to the logs") + .baseUnit(BaseUnits.EVENTS) + .register(registry); + } + + @Override + public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) { + // When filter is asked for decision for an isDebugEnabled call or similar test, there is no message (ie format) + // and no intention to log anything with this call. We will not increment counters and can return immediately and + // avoid the relatively expensive ThreadLocal access below. See also logbacks Logger.callTurboFilters(). + if (format == null) { + return FilterReply.NEUTRAL; + } + + Boolean ignored = LogbackMetrics.ignoreMetrics.get(); + if (ignored != null && ignored) { + return FilterReply.NEUTRAL; + } + + // cannot use logger.isEnabledFor(level), as it would cause a StackOverflowError by calling this filter again! + if (level.isGreaterOrEqual(logger.getEffectiveLevel())) { + switch (level.toInt()) { + case Level.ERROR_INT: + errorCounter.increment(); + break; + case Level.WARN_INT: + warnCounter.increment(); + break; + case Level.INFO_INT: + infoCounter.increment(); + break; + case Level.DEBUG_INT: + debugCounter.increment(); + break; + case Level.TRACE_INT: + traceCounter.increment(); + break; + } + } + + return FilterReply.NEUTRAL; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java new file mode 100644 index 0000000000..0ed99e6dc4 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProvider.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 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.binder.mongodb; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.mongodb.event.CommandEvent; +import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.util.StringUtils; +import io.micrometer.core.util.internal.logging.WarnThenDebugLogger; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; + +/** + * Default implementation for {@link MongoCommandTagsProvider}. + * + * @author Chris Bono + * @since 1.7.0 + */ +public class DefaultMongoCommandTagsProvider implements MongoCommandTagsProvider { + + // See https://docs.mongodb.com/manual/reference/command for the command reference + private static final Set COMMANDS_WITH_COLLECTION_NAME = new HashSet<>(Arrays.asList( + "aggregate", "count", "distinct", "mapReduce", "geoSearch", "delete", "find", "findAndModify", + "insert", "update", "collMod", "compact", "convertToCapped", "create", "createIndexes", "drop", + "dropIndexes", "killCursors", "listIndexes", "reIndex")); + + private static final WarnThenDebugLogger WARN_THEN_DEBUG_LOGGER = new WarnThenDebugLogger(DefaultMongoCommandTagsProvider.class); + + private final ConcurrentMap inFlightCommandCollectionNames = new ConcurrentHashMap<>(); + + @Override + public Iterable commandTags(CommandEvent event) { + return Tags.of( + Tag.of("command", event.getCommandName()), + Tag.of("collection", getAndRemoveCollectionNameForCommand(event)), + Tag.of("cluster.id", event.getConnectionDescription().getConnectionId().getServerId().getClusterId().getValue()), + Tag.of("server.address", event.getConnectionDescription().getServerAddress().toString()), + Tag.of("status", (event instanceof CommandSucceededEvent) ? "SUCCESS" : "FAILED")); + } + + @Override + public void commandStarted(CommandStartedEvent event) { + determineCollectionName(event.getCommandName(), event.getCommand()) + .ifPresent(collectionName -> addCollectionNameForCommand(event, collectionName)); + } + + private void addCollectionNameForCommand(CommandEvent event, String collectionName) { + if (inFlightCommandCollectionNames.size() < 1000) { + inFlightCommandCollectionNames.put(event.getRequestId(), collectionName); + return; + } + // Cache over capacity + WARN_THEN_DEBUG_LOGGER.log("Collection names cache is full - Mongo is not calling listeners properly"); + } + + private String getAndRemoveCollectionNameForCommand(CommandEvent event) { + String collectionName = inFlightCommandCollectionNames.remove(event.getRequestId()); + return collectionName != null ? collectionName : "unknown"; + } + + /** + * Attempts to determine the name of the collection a command is operating on. + * + *

Because some commands either do not have collection info or it is problematic to determine the collection info, + * there is an allow list of command names {@code COMMANDS_WITH_COLLECTION_NAME} used. If {@code commandName} is + * not in the allow list or there is no collection info in {@code command}, it will use the content of the + * {@code 'collection'} field on {@code command}, if it exists. + * + *

Taken from TraceMongoCommandListener.java in Brave + * + * @param commandName name of the mongo command + * @param command mongo command object + * @return optional collection name or empty if could not be determined or not in the allow list of command names + */ + protected Optional determineCollectionName(String commandName, BsonDocument command) { + if (COMMANDS_WITH_COLLECTION_NAME.contains(commandName)) { + Optional collectionName = getNonEmptyBsonString(command.get(commandName)); + if (collectionName.isPresent()) { + return collectionName; + } + } + // Some other commands, like getMore, have a field like {"collection": collectionName}. + return getNonEmptyBsonString(command.get("collection")); + } + + /** + * @return trimmed string from {@code bsonValue} in the Optional or empty Optional if value was not a non-empty string + */ + private Optional getNonEmptyBsonString(BsonValue bsonValue) { + return Optional.ofNullable(bsonValue) + .filter(BsonValue::isString) + .map(BsonValue::asString) + .map(BsonString::getValue) + .map(String::trim) + .filter(StringUtils::isNotEmpty); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java new file mode 100644 index 0000000000..e156b8a60b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 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.binder.mongodb; + +import com.mongodb.event.ConnectionPoolCreatedEvent; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +/** + * Default implementation for {@link MongoConnectionPoolTagsProvider}. + * + * @author Gustavo Monarin + * @since 1.7.0 + */ +public class DefaultMongoConnectionPoolTagsProvider implements MongoConnectionPoolTagsProvider { + + @Override + public Iterable connectionPoolTags(final ConnectionPoolCreatedEvent event) { + return Tags.of( + Tag.of("cluster.id", event.getServerId().getClusterId().getValue()), + Tag.of("server.address", event.getServerId().getAddress().toString())); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandTagsProvider.java new file mode 100644 index 0000000000..d88ae98a65 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoCommandTagsProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 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.binder.mongodb; + +import com.mongodb.event.CommandEvent; +import com.mongodb.event.CommandStartedEvent; +import io.micrometer.core.instrument.Tag; + +/** + * Provides {@link Tag Tags} for Mongo command metrics. + * + * @author Chris Bono + * @since 1.7.0 + */ +@FunctionalInterface +public interface MongoCommandTagsProvider { + + /** + * Signals that a command has started and is a chance for implementations to prepare + * or do any necessary pre-processing. + * + * @param commandStartedEvent event representing the issued command + * @since 1.8.0 + */ + default void commandStarted(CommandStartedEvent commandStartedEvent) { + } + + /** + * Provides tags to be associated with metrics for the given Mongo command. + * + * @param commandEvent event representing the issued command + * @return tags to associate with metrics recorded for the command + */ + Iterable commandTags(CommandEvent commandEvent); +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoConnectionPoolTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoConnectionPoolTagsProvider.java new file mode 100644 index 0000000000..5c8faa5b7e --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoConnectionPoolTagsProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020 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.binder.mongodb; + +import com.mongodb.event.ConnectionPoolCreatedEvent; +import io.micrometer.core.instrument.Tag; + +/** + * Provides {@link Tag Tags} for Mongo connection pool metrics. + * + * @author Gustavo Monarin + * @since 1.7.0 + */ +@FunctionalInterface +public interface MongoConnectionPoolTagsProvider { + + /** + * Provides tags to be associated with the Mongo connection metrics for the given {@link ConnectionPoolCreatedEvent event}. + * + * @param event The Mongo event of when the connection pool is opened + * @return tags to be associated with metrics recorded for the connection pool + */ + Iterable connectionPoolTags(ConnectionPoolCreatedEvent event); +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsCommandListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsCommandListener.java new file mode 100644 index 0000000000..c4bf4e8c86 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsCommandListener.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 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.binder.mongodb; + +import java.util.concurrent.TimeUnit; + +import com.mongodb.client.MongoClient; +import com.mongodb.event.CommandEvent; +import com.mongodb.event.CommandFailedEvent; +import com.mongodb.event.CommandListener; +import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * {@link CommandListener} for collecting command metrics from {@link MongoClient}. + * + * @author Christophe Bornet + * @author Chris Bono + * @since 1.2.0 + */ +@NonNullApi +@NonNullFields +@Incubating(since = "1.2.0") +public class MongoMetricsCommandListener implements CommandListener { + + private final MeterRegistry registry; + + private final MongoCommandTagsProvider tagsProvider; + + /** + * Constructs a command listener that uses the default tags provider. + * + * @param registry meter registry + */ + public MongoMetricsCommandListener(MeterRegistry registry) { + this(registry, new DefaultMongoCommandTagsProvider()); + } + + /** + * Constructs a command listener with a custom tags provider. + * + * @param registry meter registry + * @param tagsProvider provides tags to be associated with metrics for the given Mongo command + * @since 1.7.0 + */ + public MongoMetricsCommandListener(MeterRegistry registry, MongoCommandTagsProvider tagsProvider) { + this.registry = registry; + this.tagsProvider = tagsProvider; + } + + @Override + public void commandStarted(CommandStartedEvent commandStartedEvent) { + tagsProvider.commandStarted(commandStartedEvent); + } + + @Override + public void commandSucceeded(CommandSucceededEvent event) { + timeCommand(event, event.getElapsedTime(TimeUnit.NANOSECONDS)); + } + + @Override + public void commandFailed(CommandFailedEvent event) { + timeCommand(event, event.getElapsedTime(TimeUnit.NANOSECONDS)); + } + + private void timeCommand(CommandEvent event, long elapsedTimeInNanoseconds) { + Timer.builder("mongodb.driver.commands") + .description("Timer of mongodb commands") + .tags(tagsProvider.commandTags(event)) + .register(registry) + .record(elapsedTimeInNanoseconds, TimeUnit.NANOSECONDS); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListener.java new file mode 100644 index 0000000000..02c5569018 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListener.java @@ -0,0 +1,172 @@ +/* + * Copyright 2019 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.binder.mongodb; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import com.mongodb.client.MongoClient; +import com.mongodb.connection.ServerId; +import com.mongodb.event.ConnectionCheckOutFailedEvent; +import com.mongodb.event.ConnectionCheckOutStartedEvent; +import com.mongodb.event.ConnectionCheckedInEvent; +import com.mongodb.event.ConnectionCheckedOutEvent; +import com.mongodb.event.ConnectionClosedEvent; +import com.mongodb.event.ConnectionCreatedEvent; +import com.mongodb.event.ConnectionPoolClosedEvent; +import com.mongodb.event.ConnectionPoolCreatedEvent; +import com.mongodb.event.ConnectionPoolListener; +import io.micrometer.core.annotation.Incubating; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * {@link ConnectionPoolListener} for collecting connection pool metrics from {@link MongoClient}. + * + * @author Christophe Bornet + * @author Jonatan Ivanov + * @since 1.2.0 + * @implNote This implementation requires MongoDB Java driver 4 or later. + */ +@NonNullApi +@NonNullFields +@Incubating(since = "1.2.0") +public class MongoMetricsConnectionPoolListener implements ConnectionPoolListener { + + private static final String METRIC_PREFIX = "mongodb.driver.pool."; + + private final Map poolSizes = new ConcurrentHashMap<>(); + private final Map checkedOutCounts = new ConcurrentHashMap<>(); + private final Map waitQueueSizes = new ConcurrentHashMap<>(); + private final Map> meters = new ConcurrentHashMap<>(); + + private final MeterRegistry registry; + private final MongoConnectionPoolTagsProvider tagsProvider; + + /** + * Create a new {@code MongoMetricsConnectionPoolListener}. + * + * @param registry registry to use + */ + public MongoMetricsConnectionPoolListener(MeterRegistry registry) { + this(registry, new DefaultMongoConnectionPoolTagsProvider()); + } + + /** + * Create a new {@code MongoMetricsConnectionPoolListener}. + * + * @param registry registry to use + * @param tagsProvider tags provider to use + * @since 1.7.0 + */ + public MongoMetricsConnectionPoolListener(MeterRegistry registry, MongoConnectionPoolTagsProvider tagsProvider) { + this.registry = registry; + this.tagsProvider = tagsProvider; + } + + @Override + public void connectionPoolCreated(ConnectionPoolCreatedEvent event) { + List connectionMeters = new ArrayList<>(); + connectionMeters.add(registerGauge(event, METRIC_PREFIX + "size", + "the current size of the connection pool, including idle and and in-use members", poolSizes)); + connectionMeters.add(registerGauge(event, METRIC_PREFIX + "checkedout", + "the count of connections that are currently in use", checkedOutCounts)); + connectionMeters.add(registerGauge(event, METRIC_PREFIX + "waitqueuesize", + "the current size of the wait queue for a connection from the pool", waitQueueSizes)); + meters.put(event.getServerId(), connectionMeters); + } + + @Override + public void connectionPoolClosed(ConnectionPoolClosedEvent event) { + ServerId serverId = event.getServerId(); + for (Meter meter : meters.get(serverId)) { + registry.remove(meter); + } + meters.remove(serverId); + poolSizes.remove(serverId); + checkedOutCounts.remove(serverId); + waitQueueSizes.remove(serverId); + } + + @Override + public void connectionCheckOutStarted(ConnectionCheckOutStartedEvent event) { + AtomicInteger waitQueueSize = waitQueueSizes.get(event.getServerId()); + if (waitQueueSize != null) { + waitQueueSize.incrementAndGet(); + } + } + + @Override + public void connectionCheckedOut(ConnectionCheckedOutEvent event) { + AtomicInteger checkedOutCount = checkedOutCounts.get(event.getConnectionId().getServerId()); + if (checkedOutCount != null) { + checkedOutCount.incrementAndGet(); + } + + AtomicInteger waitQueueSize = waitQueueSizes.get(event.getConnectionId().getServerId()); + if (waitQueueSize != null) { + waitQueueSize.decrementAndGet(); + } + } + + @Override + public void connectionCheckOutFailed(ConnectionCheckOutFailedEvent event) { + AtomicInteger waitQueueSize = waitQueueSizes.get(event.getServerId()); + if (waitQueueSize != null) { + waitQueueSize.decrementAndGet(); + } + } + + @Override + public void connectionCheckedIn(ConnectionCheckedInEvent event) { + AtomicInteger checkedOutCount = checkedOutCounts.get(event.getConnectionId().getServerId()); + if (checkedOutCount != null) { + checkedOutCount.decrementAndGet(); + } + } + + @Override + public void connectionCreated(ConnectionCreatedEvent event) { + AtomicInteger poolSize = poolSizes.get(event.getConnectionId().getServerId()); + if (poolSize != null) { + poolSize.incrementAndGet(); + } + } + + @Override + public void connectionClosed(ConnectionClosedEvent event) { + AtomicInteger poolSize = poolSizes.get(event.getConnectionId().getServerId()); + if (poolSize != null) { + poolSize.decrementAndGet(); + } + } + + private Gauge registerGauge(ConnectionPoolCreatedEvent event, String metricName, String description, Map metrics) { + AtomicInteger value = new AtomicInteger(); + metrics.put(event.getServerId(), value); + return Gauge.builder(metricName, value, AtomicInteger::doubleValue) + .description(description) + .tags(tagsProvider.connectionPoolTags(event)) + .register(registry); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetrics.java new file mode 100644 index 0000000000..28020db383 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetrics.java @@ -0,0 +1,181 @@ +/* + * Copyright 2020 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.binder.okhttp3; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; + +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNull; +import okhttp3.ConnectionPool; + +/** + * MeterBinder for collecting metrics of a given OkHttp {@link ConnectionPool}. + *

+ * Example usage: + *

+ *     ConnectionPool connectionPool = new ConnectionPool(connectionPoolSize, connectionPoolKeepAliveMs, TimeUnit.MILLISECONDS);
+ *     new OkHttpConnectionPoolMetrics(connectionPool).bindTo(registry);
+ * 
+ * + * @author Ben Hubert + * @since 1.6.0 + */ +public class OkHttpConnectionPoolMetrics implements MeterBinder { + + private static final String DEFAULT_NAME_PREFIX = "okhttp.pool"; + private static final String TAG_STATE = "state"; + + private final ConnectionPool connectionPool; + private final String namePrefix; + private final Iterable tags; + private final Double maxIdleConnectionCount; + private final ThreadLocal connectionStats = new ThreadLocal<>(); + + /** + * Creates a meter binder for the given connection pool. + * Metrics will be exposed using {@value #DEFAULT_NAME_PREFIX} as name prefix. + * + * @param connectionPool The connection pool to monitor. Must not be null. + */ + public OkHttpConnectionPoolMetrics(ConnectionPool connectionPool) { + this(connectionPool, DEFAULT_NAME_PREFIX, Collections.emptyList(), null); + } + + /** + * Creates a meter binder for the given connection pool. + * Metrics will be exposed using {@value #DEFAULT_NAME_PREFIX} as name prefix. + * + * @param connectionPool The connection pool to monitor. Must not be null. + * @param tags A list of tags which will be passed for all meters. Must not be null. + */ + public OkHttpConnectionPoolMetrics(ConnectionPool connectionPool, Iterable tags) { + this(connectionPool, DEFAULT_NAME_PREFIX, tags, null); + } + + /** + * Creates a meter binder for the given connection pool. + * + * @param connectionPool The connection pool to monitor. Must not be null. + * @param namePrefix The desired name prefix for the exposed metrics. Must not be null. + * @param tags A list of tags which will be passed for all meters. Must not be null. + */ + public OkHttpConnectionPoolMetrics(ConnectionPool connectionPool, String namePrefix, Iterable tags) { + this(connectionPool, namePrefix, tags, null); + } + + /** + * Creates a meter binder for the given connection pool. + * + * @param connectionPool The connection pool to monitor. Must not be null. + * @param namePrefix The desired name prefix for the exposed metrics. Must not be null. + * @param tags A list of tags which will be passed for all meters. Must not be null. + * @param maxIdleConnections The maximum number of idle connections this pool will hold. This + * value is passed to the {@link ConnectionPool} constructor but is + * not exposed by this instance. Therefore this binder allows to pass + * it, to be able to monitor it. + */ + public OkHttpConnectionPoolMetrics(ConnectionPool connectionPool, String namePrefix, Iterable tags, Integer maxIdleConnections) { + if (connectionPool == null) { + throw new IllegalArgumentException("Given ConnectionPool must not be null."); + } + if (namePrefix == null) { + throw new IllegalArgumentException("Given name prefix must not be null."); + } + if (tags == null) { + throw new IllegalArgumentException("Given list of tags must not be null."); + } + + this.connectionPool = connectionPool; + this.namePrefix = namePrefix; + this.tags = tags; + this.maxIdleConnectionCount = Optional.ofNullable(maxIdleConnections) + .map(Integer::doubleValue) + .orElse(null); + } + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + String connectionCountName = namePrefix + ".connection.count"; + Gauge.builder(connectionCountName, connectionStats, + cs -> { + if (cs.get() == null) { + cs.set(new ConnectionPoolConnectionStats()); + } + return cs.get().getActiveCount(); + }) + .baseUnit(BaseUnits.CONNECTIONS) + .description("The state of connections in the OkHttp connection pool") + .tags(Tags.of(tags).and(TAG_STATE, "active")) + .register(registry); + + Gauge.builder(connectionCountName, connectionStats, + cs -> { + if (cs.get() == null) { + cs.set(new ConnectionPoolConnectionStats()); + } + return cs.get().getIdleConnectionCount(); + }) + .baseUnit(BaseUnits.CONNECTIONS) + .description("The state of connections in the OkHttp connection pool") + .tags(Tags.of(tags).and(TAG_STATE, "idle")) + .register(registry); + + if (this.maxIdleConnectionCount != null) { + Gauge.builder(namePrefix + ".connection.limit", () -> this.maxIdleConnectionCount) + .baseUnit(BaseUnits.CONNECTIONS) + .description("The maximum idle connection count in an OkHttp connection pool.") + .tags(Tags.concat(tags)) + .register(registry); + } + } + + /** + * Allow us to coordinate between active and idle, making sure they always sum to the total available connections. + * Since we're calculating active from total-idle, we want to synchronize on idle to make sure the sum is accurate. + */ + private final class ConnectionPoolConnectionStats { + private CountDownLatch uses = new CountDownLatch(0); + private int idle; + private int total; + + public int getActiveCount() { + snapshotStatsIfNecessary(); + uses.countDown(); + return total - idle; + } + + public int getIdleConnectionCount() { + snapshotStatsIfNecessary(); + uses.countDown(); + return idle; + } + + private void snapshotStatsIfNecessary() { + if (uses.getCount() == 0) { + idle = connectionPool.idleConnectionCount(); + total = connectionPool.connectionCount(); + uses = new CountDownLatch(2); + } + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListener.java new file mode 100644 index 0000000000..21d54f910b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListener.java @@ -0,0 +1,354 @@ +/* + * Copyright 2017 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.binder.okhttp3; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.function.Function; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import okhttp3.Call; +import okhttp3.EventListener; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static java.util.stream.StreamSupport.stream; + +/** + * {@link EventListener} for collecting metrics from {@link OkHttpClient}. + *

+ * {@literal uri} tag is usually limited to URI patterns to mitigate tag cardinality explosion but {@link OkHttpClient} + * doesn't provide URI patterns. We provide {@value OkHttpMetricsEventListener#URI_PATTERN} header to support + * {@literal uri} tag or you can configure a {@link Builder#uriMapper(Function) URI mapper} to provide your own tag + * values for {@literal uri} tag. + * + * @author Bjarte S. Karlsen + * @author Jon Schneider + * @author Nurettin Yilmaz + * @author Johnny Lim + */ +@NonNullApi +@NonNullFields +public class OkHttpMetricsEventListener extends EventListener { + + /** + * Header name for URI patterns which will be used for tag values. + */ + public static final String URI_PATTERN = "URI_PATTERN"; + + private static final boolean REQUEST_TAG_CLASS_EXISTS; + + static { + REQUEST_TAG_CLASS_EXISTS = getMethod(Class.class) != null; + } + + private static final String TAG_TARGET_SCHEME = "target.scheme"; + private static final String TAG_TARGET_HOST = "target.host"; + private static final String TAG_TARGET_PORT = "target.port"; + + private static final String TAG_VALUE_UNKNOWN = "UNKNOWN"; + + private static final Tags TAGS_TARGET_UNKNOWN = Tags.of( + TAG_TARGET_SCHEME, TAG_VALUE_UNKNOWN, + TAG_TARGET_HOST, TAG_VALUE_UNKNOWN, + TAG_TARGET_PORT, TAG_VALUE_UNKNOWN + ); + + @Nullable + private static Method getMethod(Class... parameterTypes) { + try { + return Request.class.getMethod("tag", parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + + private final MeterRegistry registry; + private final String requestsMetricName; + private final Function urlMapper; + private final Iterable extraTags; + private final Iterable> contextSpecificTags; + private final Iterable unknownRequestTags; + private final boolean includeHostTag; + + // VisibleForTesting + final ConcurrentMap callState = new ConcurrentHashMap<>(); + + protected OkHttpMetricsEventListener(MeterRegistry registry, String requestsMetricName, Function urlMapper, + Iterable extraTags, + Iterable> contextSpecificTags) { + this(registry, requestsMetricName, urlMapper, extraTags, contextSpecificTags, emptyList(), true); + } + + OkHttpMetricsEventListener(MeterRegistry registry, String requestsMetricName, Function urlMapper, + Iterable extraTags, + Iterable> contextSpecificTags, + Iterable requestTagKeys, + boolean includeHostTag) { + this.registry = registry; + this.requestsMetricName = requestsMetricName; + this.urlMapper = urlMapper; + this.extraTags = extraTags; + this.contextSpecificTags = contextSpecificTags; + this.includeHostTag = includeHostTag; + + List unknownRequestTags = new ArrayList<>(); + for (String requestTagKey : requestTagKeys) { + unknownRequestTags.add(Tag.of(requestTagKey, "UNKNOWN")); + } + this.unknownRequestTags = unknownRequestTags; + } + + public static Builder builder(MeterRegistry registry, String name) { + return new Builder(registry, name); + } + + @Override + public void callStart(Call call) { + callState.put(call, new CallState(registry.config().clock().monotonicTime(), call.request())); + } + + @Override + public void callFailed(Call call, IOException e) { + CallState state = callState.remove(call); + if (state != null) { + state.exception = e; + time(state); + } + } + + @Override + public void callEnd(Call call) { + callState.remove(call); + } + + @Override + public void responseHeadersEnd(Call call, Response response) { + CallState state = callState.remove(call); + if (state != null) { + state.response = response; + time(state); + } + } + + // VisibleForTesting + void time(CallState state) { + Request request = state.request; + boolean requestAvailable = request != null; + + Iterable tags = Tags.of( + "method", requestAvailable ? request.method() : TAG_VALUE_UNKNOWN, + "uri", getUriTag(state, request), + "status", getStatusMessage(state.response, state.exception) + ) + .and(extraTags) + .and(stream(contextSpecificTags.spliterator(), false) + .map(contextTag -> contextTag.apply(request, state.response)) + .collect(toList())) + .and(getRequestTags(request)) + .and(generateTagsForRoute(request)); + + if (includeHostTag) { + tags = Tags.of(tags).and("host", requestAvailable ? request.url().host() : TAG_VALUE_UNKNOWN); + } + + Timer.builder(this.requestsMetricName) + .tags(tags) + .description("Timer of OkHttp operation") + .register(registry) + .record(registry.config().clock().monotonicTime() - state.startTime, TimeUnit.NANOSECONDS); + } + + private Tags generateTagsForRoute(@Nullable Request request) { + if (request == null) { + return TAGS_TARGET_UNKNOWN; + } + return Tags.of( + TAG_TARGET_SCHEME, request.url().scheme(), + TAG_TARGET_HOST, request.url().host(), + TAG_TARGET_PORT, Integer.toString(request.url().port()) + ); + } + + private String getUriTag(CallState state, @Nullable Request request) { + if (request == null) { + return TAG_VALUE_UNKNOWN; + } + return state.response != null && (state.response.code() == 404 || state.response.code() == 301) + ? "NOT_FOUND" : urlMapper.apply(request); + } + + private Iterable getRequestTags(@Nullable Request request) { + if (request == null) { + return unknownRequestTags; + } + if (REQUEST_TAG_CLASS_EXISTS) { + Tags requestTag = request.tag(Tags.class); + if (requestTag != null) { + return requestTag; + } + } + Object requestTag = request.tag(); + if (requestTag instanceof Tags) { + return (Tags) requestTag; + } + return Tags.empty(); + } + + private String getStatusMessage(@Nullable Response response, @Nullable IOException exception) { + if (exception != null) { + return "IO_ERROR"; + } + + if (response == null) { + return "CLIENT_ERROR"; + } + + return Integer.toString(response.code()); + } + + // VisibleForTesting + static class CallState { + final long startTime; + @Nullable + final Request request; + @Nullable + Response response; + @Nullable + IOException exception; + + CallState(long startTime, @Nullable Request request) { + this.startTime = startTime; + this.request = request; + } + } + + public static class Builder { + private final MeterRegistry registry; + private final String name; + private Function uriMapper = (request) -> Optional.ofNullable(request.header(URI_PATTERN)).orElse("none"); + private Tags tags = Tags.empty(); + private Collection> contextSpecificTags = new ArrayList<>(); + private boolean includeHostTag = true; + private Iterable requestTagKeys = Collections.emptyList(); + + Builder(MeterRegistry registry, String name) { + this.registry = registry; + this.name = name; + } + + public Builder tags(Iterable tags) { + this.tags = this.tags.and(tags); + return this; + } + + /** + * Add a {@link Tag} to any already configured tags on this Builder. + * + * @param tag tag to add + * @return this builder + * @since 1.5.0 + */ + public Builder tag(Tag tag) { + this.tags = this.tags.and(tag); + return this; + } + + /** + * Add a context-specific tag. + * + * @param contextSpecificTag function to create a context-specific tag + * @return this builder + * @since 1.5.0 + */ + public Builder tag(BiFunction contextSpecificTag) { + this.contextSpecificTags.add(contextSpecificTag); + return this; + } + + public Builder uriMapper(Function uriMapper) { + this.uriMapper = uriMapper; + return this; + } + + /** + * Historically, OkHttp Metrics provided by {@link OkHttpMetricsEventListener} included a + * {@code host} tag for the target host being called. To align with other HTTP client metrics, + * this was changed to {@code target.host}, but to maintain backwards compatibility the {@code host} + * tag can also be included. By default, {@code includeHostTag} is {@literal true} so both tags are included. + * + * @param includeHostTag whether to include the {@code host} tag + * @return this builder + * @since 1.5.0 + */ + public Builder includeHostTag(boolean includeHostTag) { + this.includeHostTag = includeHostTag; + return this; + } + + /** + * Tag keys for {@link Request#tag()} or {@link Request#tag(Class)}. + * + * These keys will be added with {@literal UNKNOWN} values when {@link Request} is {@literal null}. + * Note that this is required only for Prometheus as it requires tag match for the same metric. + * + * @param requestTagKeys request tag keys + * @return this builder + * @since 1.3.9 + */ + public Builder requestTagKeys(String... requestTagKeys) { + return requestTagKeys(Arrays.asList(requestTagKeys)); + } + + /** + * Tag keys for {@link Request#tag()} or {@link Request#tag(Class)}. + * + * These keys will be added with {@literal UNKNOWN} values when {@link Request} is {@literal null}. + * Note that this is required only for Prometheus as it requires tag match for the same metric. + * + * @param requestTagKeys request tag keys + * @return this builder + * @since 1.3.9 + */ + public Builder requestTagKeys(Iterable requestTagKeys) { + this.requestTagKeys = requestTagKeys; + return this; + } + + public OkHttpMetricsEventListener build() { + return new OkHttpMetricsEventListener(registry, name, uriMapper, tags, contextSpecificTags, requestTagKeys, includeHostTag); + } + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/system/DiskSpaceMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/system/DiskSpaceMetrics.java new file mode 100644 index 0000000000..e1cf46b0d6 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/system/DiskSpaceMetrics.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 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.binder.system; + +import java.io.File; + +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.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Collections.emptyList; + +/** + * Record metrics that report disk space usage. + * + * @author jmcshane + * @author Johnny Lim + * @since 1.8.0 + */ +@NonNullApi +@NonNullFields +public class DiskSpaceMetrics implements MeterBinder { + private final Iterable tags; + private final File path; + private final String absolutePath; + + public DiskSpaceMetrics(File path) { + this(path, emptyList()); + } + + public DiskSpaceMetrics(File path, Iterable tags) { + this.path = path; + this.absolutePath = path.getAbsolutePath(); + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + Iterable tagsWithPath = Tags.concat(tags, "path", absolutePath); + Gauge.builder("disk.free", path, File::getUsableSpace) + .tags(tagsWithPath) + .description("Usable space for path") + .baseUnit(BaseUnits.BYTES) + .strongReference(true) + .register(registry); + Gauge.builder("disk.total", path, File::getTotalSpace) + .tags(tagsWithPath) + .description("Total space for path") + .baseUnit(BaseUnits.BYTES) + .strongReference(true) + .register(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/system/FileDescriptorMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/system/FileDescriptorMetrics.java new file mode 100644 index 0000000000..3dc1f7767b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/system/FileDescriptorMetrics.java @@ -0,0 +1,141 @@ +/* + * Copyright 2017 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.binder.system; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +import static java.util.Collections.emptyList; + +/** + * File descriptor metrics gathered by the JVM. + *

+ * Supported JVM implementations: + *

    + *
  • HotSpot
  • + *
  • J9
  • + *
+ * + * @author Michael Weirauch + * @author Tommy Ludwig + */ +@NonNullApi +@NonNullFields +public class FileDescriptorMetrics implements MeterBinder { + + /** + * List of public, exported interface class names from supported JVM implementations. + */ + private static final List UNIX_OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList( + "com.sun.management.UnixOperatingSystemMXBean", // HotSpot + "com.ibm.lang.management.UnixOperatingSystemMXBean" // J9 + ); + + private final OperatingSystemMXBean osBean; + private final Iterable tags; + + @Nullable + private final Class osBeanClass; + + @Nullable + private final Method openFilesMethod; + + @Nullable + private final Method maxFilesMethod; + + public FileDescriptorMetrics() { + this(emptyList()); + } + + public FileDescriptorMetrics(Iterable tags) { + this(ManagementFactory.getOperatingSystemMXBean(), tags); + } + + // VisibleForTesting + FileDescriptorMetrics(OperatingSystemMXBean osBean, Iterable tags) { + this.osBean = osBean; + this.tags = tags; + + this.osBeanClass = getFirstClassFound(UNIX_OPERATING_SYSTEM_BEAN_CLASS_NAMES); + this.openFilesMethod = detectMethod("getOpenFileDescriptorCount"); + this.maxFilesMethod = detectMethod("getMaxFileDescriptorCount"); + } + + @Override + public void bindTo(MeterRegistry registry) { + if (openFilesMethod != null) { + Gauge.builder("process.files.open", osBean, x -> invoke(openFilesMethod)) + .tags(tags) + .description("The open file descriptor count") + .baseUnit(BaseUnits.FILES) + .register(registry); + } + + if (maxFilesMethod != null) { + Gauge.builder("process.files.max", osBean, x -> invoke(maxFilesMethod)) + .tags(tags) + .description("The maximum file descriptor count") + .baseUnit(BaseUnits.FILES) + .register(registry); + } + } + + private double invoke(@Nullable Method method) { + try { + return method != null ? (double) (long) method.invoke(osBean) : Double.NaN; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return Double.NaN; + } + } + + @Nullable + private Method detectMethod(String name) { + if (osBeanClass == null) { + return null; + } + try { + // ensure the Bean we have is actually an instance of the interface + osBeanClass.cast(osBean); + return osBeanClass.getDeclaredMethod(name); + } catch (ClassCastException | NoSuchMethodException | SecurityException e) { + return null; + } + } + + @Nullable + private Class getFirstClassFound(List classNames) { + for (String className : classNames) { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignore) { + } + } + return null; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/system/ProcessorMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/system/ProcessorMetrics.java new file mode 100644 index 0000000000..d995c305ff --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/system/ProcessorMetrics.java @@ -0,0 +1,150 @@ +/* + * Copyright 2017 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.binder.system; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; + +/** + * Record metrics related to the CPU, gathered by the JVM. + *

+ * Supported JVM implementations: + *

    + *
  • HotSpot
  • + *
  • J9
  • + *
+ * + * @author Jon Schneider + * @author Michael Weirauch + * @author Clint Checketts + * @author Tommy Ludwig + */ +@NonNullApi +@NonNullFields +public class ProcessorMetrics implements MeterBinder { + + /** List of public, exported interface class names from supported JVM implementations. */ + private static final List OPERATING_SYSTEM_BEAN_CLASS_NAMES = Arrays.asList( + "com.ibm.lang.management.OperatingSystemMXBean", // J9 + "com.sun.management.OperatingSystemMXBean" // HotSpot + ); + + private final Iterable tags; + + private final OperatingSystemMXBean operatingSystemBean; + + @Nullable + private final Class operatingSystemBeanClass; + + @Nullable + private final Method systemCpuUsage; + + @Nullable + private final Method processCpuUsage; + + public ProcessorMetrics() { + this(emptyList()); + } + + public ProcessorMetrics(Iterable tags) { + this.tags = tags; + this.operatingSystemBean = ManagementFactory.getOperatingSystemMXBean(); + this.operatingSystemBeanClass = getFirstClassFound(OPERATING_SYSTEM_BEAN_CLASS_NAMES); + Method getCpuLoad = detectMethod("getCpuLoad"); + this.systemCpuUsage = getCpuLoad != null ? getCpuLoad : detectMethod("getSystemCpuLoad"); + this.processCpuUsage = detectMethod("getProcessCpuLoad"); + } + + @Override + public void bindTo(MeterRegistry registry) { + Runtime runtime = Runtime.getRuntime(); + Gauge.builder("system.cpu.count", runtime, Runtime::availableProcessors) + .tags(tags) + .description("The number of processors available to the Java virtual machine") + .register(registry); + + if (operatingSystemBean.getSystemLoadAverage() >= 0) { + Gauge.builder("system.load.average.1m", operatingSystemBean, OperatingSystemMXBean::getSystemLoadAverage) + .tags(tags) + .description("The sum of the number of runnable entities queued to available processors and the number " + + "of runnable entities running on the available processors averaged over a period of time") + .register(registry); + } + + if (systemCpuUsage != null) { + Gauge.builder("system.cpu.usage", operatingSystemBean, x -> invoke(systemCpuUsage)) + .tags(tags) + .description("The \"recent cpu usage\" of the system the application is running in") + .register(registry); + } + + if (processCpuUsage != null) { + Gauge.builder("process.cpu.usage", operatingSystemBean, x -> invoke(processCpuUsage)) + .tags(tags) + .description("The \"recent cpu usage\" for the Java Virtual Machine process") + .register(registry); + } + } + + private double invoke(@Nullable Method method) { + try { + return method != null ? (double) method.invoke(operatingSystemBean) : Double.NaN; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return Double.NaN; + } + } + + @Nullable + private Method detectMethod(String name) { + requireNonNull(name); + if (operatingSystemBeanClass == null) { + return null; + } + try { + // ensure the Bean we have is actually an instance of the interface + operatingSystemBeanClass.cast(operatingSystemBean); + return operatingSystemBeanClass.getDeclaredMethod(name); + } catch (ClassCastException | NoSuchMethodException | SecurityException e) { + return null; + } + } + + @Nullable + private Class getFirstClassFound(List classNames) { + for (String className : classNames) { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignore) { + } + } + return null; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/system/UptimeMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/system/UptimeMetrics.java new file mode 100644 index 0000000000..662032fdeb --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/system/UptimeMetrics.java @@ -0,0 +1,69 @@ +/* + * Copyright 2017 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.binder.system; + +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +import static java.util.Collections.emptyList; + +/** + * Uptime metrics. + * + * @author Michael Weirauch + */ +@NonNullApi +@NonNullFields +public class UptimeMetrics implements MeterBinder { + + private final RuntimeMXBean runtimeMXBean; + private final Iterable tags; + + public UptimeMetrics() { + this(emptyList()); + } + + public UptimeMetrics(Iterable tags) { + this(ManagementFactory.getRuntimeMXBean(), tags); + } + + // VisibleForTesting + UptimeMetrics(RuntimeMXBean runtimeMXBean, Iterable tags) { + this.runtimeMXBean = runtimeMXBean; + this.tags = tags; + } + + @Override + public void bindTo(MeterRegistry registry) { + TimeGauge.builder("process.uptime", runtimeMXBean, TimeUnit.MILLISECONDS, RuntimeMXBean::getUptime) + .tags(tags) + .description("The uptime of the Java virtual machine") + .register(registry); + + TimeGauge.builder("process.start.time", runtimeMXBean, TimeUnit.MILLISECONDS, RuntimeMXBean::getStartTime) + .tags(tags) + .description("Start time of the process since unix epoch.") + .register(registry); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/tomcat/TomcatMetrics.java b/micrometer-binders/src/main/java/io/micrometer/binder/tomcat/TomcatMetrics.java new file mode 100644 index 0000000000..368ed874fe --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/tomcat/TomcatMetrics.java @@ -0,0 +1,395 @@ +/* + * Copyright 2017 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.binder.tomcat; + +import java.lang.management.ManagementFactory; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +import javax.management.InstanceNotFoundException; +import javax.management.ListenerNotFoundException; +import javax.management.MBeanServer; +import javax.management.MBeanServerDelegate; +import javax.management.MBeanServerFactory; +import javax.management.MBeanServerNotification; +import javax.management.MalformedObjectNameException; +import javax.management.Notification; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; +import javax.management.ObjectName; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.FunctionTimer; +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.TimeGauge; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.lang.Nullable; +import org.apache.catalina.Manager; + +/** + * {@link MeterBinder} for Tomcat. + *

Note: the {@link #close()} method should be called when the application shuts down + * to clean up listeners this binder registers. + * + * @author Clint Checketts + * @author Jon Schneider + * @author Johnny Lim + */ +@NonNullApi +@NonNullFields +public class TomcatMetrics implements MeterBinder, AutoCloseable { + + private static final String JMX_DOMAIN_EMBEDDED = "Tomcat"; + private static final String JMX_DOMAIN_STANDALONE = "Catalina"; + private static final String OBJECT_NAME_SERVER_SUFFIX = ":type=Server"; + private static final String OBJECT_NAME_SERVER_EMBEDDED = JMX_DOMAIN_EMBEDDED + OBJECT_NAME_SERVER_SUFFIX; + private static final String OBJECT_NAME_SERVER_STANDALONE = JMX_DOMAIN_STANDALONE + OBJECT_NAME_SERVER_SUFFIX; + + @Nullable + private final Manager manager; + + private final MBeanServer mBeanServer; + private final Iterable tags; + + private final Set notificationListeners = ConcurrentHashMap.newKeySet(); + + private volatile String jmxDomain; + + public TomcatMetrics(@Nullable Manager manager, Iterable tags) { + this(manager, tags, getMBeanServer()); + } + + public TomcatMetrics(@Nullable Manager manager, Iterable tags, MBeanServer mBeanServer) { + this.manager = manager; + this.tags = tags; + this.mBeanServer = mBeanServer; + + if (manager != null) { + this.jmxDomain = manager.getContext().getDomain(); + } + } + + public static void monitor(MeterRegistry registry, @Nullable Manager manager, String... tags) { + monitor(registry, manager, Tags.of(tags)); + } + + public static void monitor(MeterRegistry registry, @Nullable Manager manager, Iterable tags) { + new TomcatMetrics(manager, tags).bindTo(registry); + } + + public static MBeanServer getMBeanServer() { + List mBeanServers = MBeanServerFactory.findMBeanServer(null); + if (!mBeanServers.isEmpty()) { + return mBeanServers.get(0); + } + return ManagementFactory.getPlatformMBeanServer(); + } + + @Override + public void bindTo(MeterRegistry registry) { + registerGlobalRequestMetrics(registry); + registerServletMetrics(registry); + registerCacheMetrics(registry); + registerThreadPoolMetrics(registry); + registerSessionMetrics(registry); + } + + private void registerSessionMetrics(MeterRegistry registry) { + if (manager == null) { + // If the binder is created but unable to find the session manager don't register those metrics + return; + } + + Gauge.builder("tomcat.sessions.active.max", manager, Manager::getMaxActive) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + Gauge.builder("tomcat.sessions.active.current", manager, Manager::getActiveSessions) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder("tomcat.sessions.created", manager, Manager::getSessionCounter) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder("tomcat.sessions.expired", manager, Manager::getExpiredSessions) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + FunctionCounter.builder("tomcat.sessions.rejected", manager, Manager::getRejectedSessions) + .tags(tags) + .baseUnit(BaseUnits.SESSIONS) + .register(registry); + + TimeGauge.builder("tomcat.sessions.alive.max", manager, TimeUnit.SECONDS, Manager::getSessionMaxAliveTime) + .tags(tags) + .register(registry); + } + + private void registerThreadPoolMetrics(MeterRegistry registry) { + registerMetricsEventually(":type=ThreadPool,name=*", (name, allTags) -> { + Gauge.builder("tomcat.threads.config.max", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "maxThreads"))) + .tags(allTags) + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder("tomcat.threads.busy", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "currentThreadsBusy"))) + .tags(allTags) + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder("tomcat.threads.current", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "currentThreadCount"))) + .tags(allTags) + .baseUnit(BaseUnits.THREADS) + .register(registry); + + Gauge.builder("tomcat.connections.current", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "connectionCount"))) + .tags(allTags) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + + Gauge.builder("tomcat.connections.keepalive.current", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "keepAliveCount"))) + .tags(allTags) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + + Gauge.builder("tomcat.connections.config.max", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "maxConnections"))) + .tags(allTags) + .baseUnit(BaseUnits.CONNECTIONS) + .register(registry); + }); + } + + private void registerCacheMetrics(MeterRegistry registry) { + registerMetricsEventually(":type=StringCache", (name, allTags) -> { + FunctionCounter.builder("tomcat.cache.access", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "accessCount"))) + .tags(allTags) + .register(registry); + + FunctionCounter.builder("tomcat.cache.hit", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "hitCount"))) + .tags(allTags) + .register(registry); + }); + } + + private void registerServletMetrics(MeterRegistry registry) { + registerMetricsEventually(":j2eeType=Servlet,name=*,*", (name, allTags) -> { + FunctionCounter.builder("tomcat.servlet.error", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "errorCount"))) + .tags(allTags) + .register(registry); + + FunctionTimer.builder("tomcat.servlet.request", mBeanServer, + s -> safeLong(() -> s.getAttribute(name, "requestCount")), + s -> safeDouble(() -> s.getAttribute(name, "processingTime")), TimeUnit.MILLISECONDS) + .tags(allTags) + .register(registry); + + TimeGauge.builder("tomcat.servlet.request.max", mBeanServer, TimeUnit.MILLISECONDS, + s -> safeDouble(() -> s.getAttribute(name, "maxTime"))) + .tags(allTags) + .register(registry); + }); + } + + private void registerGlobalRequestMetrics(MeterRegistry registry) { + registerMetricsEventually(":type=GlobalRequestProcessor,name=*", (name, allTags) -> { + FunctionCounter.builder("tomcat.global.sent", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "bytesSent"))) + .tags(allTags) + .baseUnit(BaseUnits.BYTES) + .register(registry); + + FunctionCounter.builder("tomcat.global.received", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "bytesReceived"))) + .tags(allTags) + .baseUnit(BaseUnits.BYTES) + .register(registry); + + FunctionCounter.builder("tomcat.global.error", mBeanServer, + s -> safeDouble(() -> s.getAttribute(name, "errorCount"))) + .tags(allTags) + .register(registry); + + FunctionTimer.builder("tomcat.global.request", mBeanServer, + s -> safeLong(() -> s.getAttribute(name, "requestCount")), + s -> safeDouble(() -> s.getAttribute(name, "processingTime")), TimeUnit.MILLISECONDS) + .tags(allTags) + .register(registry); + + TimeGauge.builder("tomcat.global.request.max", mBeanServer, TimeUnit.MILLISECONDS, + s -> safeDouble(() -> s.getAttribute(name, "maxTime"))) + .tags(allTags) + .register(registry); + }); + } + + /** + * If the Tomcat MBeans already exist, register metrics immediately. Otherwise register an MBean registration listener + * with the MBeanServer and register metrics when/if the MBeans becomes available. + */ + private void registerMetricsEventually(String namePatternSuffix, BiConsumer> perObject) { + if (getJmxDomain() != null) { + Set objectNames = this.mBeanServer.queryNames(getNamePattern(namePatternSuffix), null); + if (!objectNames.isEmpty()) { + // MBeans are present, so we can register metrics now. + objectNames.forEach(objectName -> perObject.accept(objectName, Tags.concat(tags, nameTag(objectName)))); + return; + } + } + + // MBean isn't yet registered, so we'll set up a notification to wait for them to be present and register + // metrics later. + NotificationListener notificationListener = new NotificationListener() { + @Override + public void handleNotification(Notification notification, Object handback) { + MBeanServerNotification mBeanServerNotification = (MBeanServerNotification) notification; + ObjectName objectName = mBeanServerNotification.getMBeanName(); + perObject.accept(objectName, Tags.concat(tags, nameTag(objectName))); + if (getNamePattern(namePatternSuffix).isPattern()) { + // patterns can match multiple MBeans so don't remove listener + return; + } + try { + mBeanServer.removeNotificationListener(MBeanServerDelegate.DELEGATE_NAME, this); + notificationListeners.remove(this); + } catch (InstanceNotFoundException | ListenerNotFoundException ex) { + throw new RuntimeException(ex); + } + } + }; + notificationListeners.add(notificationListener); + + NotificationFilter notificationFilter = (NotificationFilter) notification -> { + if (!MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notification.getType())) { + return false; + } + + // we can safely downcast now + ObjectName objectName = ((MBeanServerNotification) notification).getMBeanName(); + return getNamePattern(namePatternSuffix).apply(objectName); + }; + + try { + mBeanServer.addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, notificationListener, notificationFilter, null); + } catch (InstanceNotFoundException e) { + // should never happen + throw new RuntimeException("Error registering MBean listener", e); + } + } + + private ObjectName getNamePattern(String namePatternSuffix) { + try { + return new ObjectName(getJmxDomain() + namePatternSuffix); + } catch (MalformedObjectNameException e) { + // should never happen + throw new RuntimeException("Error registering Tomcat JMX based metrics", e); + } + } + + private String getJmxDomain() { + if (this.jmxDomain == null) { + if (hasObjectName(OBJECT_NAME_SERVER_EMBEDDED)) { + this.jmxDomain = JMX_DOMAIN_EMBEDDED; + } else if (hasObjectName(OBJECT_NAME_SERVER_STANDALONE)) { + this.jmxDomain = JMX_DOMAIN_STANDALONE; + } + } + return this.jmxDomain; + } + + /** + * Set JMX domain. If unset, default values will be used as follows: + * + *

    + *
  • Embedded Tomcat: "Tomcat"
  • + *
  • Standalone Tomcat: "Catalina"
  • + *
+ * + * @param jmxDomain JMX domain to be used + * @since 1.0.11 + */ + public void setJmxDomain(String jmxDomain) { + this.jmxDomain = jmxDomain; + } + + private boolean hasObjectName(String name) { + try { + return this.mBeanServer.queryNames(new ObjectName(name), null).size() == 1; + } catch (MalformedObjectNameException ex) { + throw new RuntimeException(ex); + } + } + + private double safeDouble(Callable callable) { + try { + return Double.parseDouble(callable.call().toString()); + } catch (Exception e) { + return Double.NaN; + } + } + + private long safeLong(Callable callable) { + try { + return Long.parseLong(callable.call().toString()); + } catch (Exception e) { + return 0; + } + } + + private Iterable nameTag(ObjectName name) { + String nameTagValue = name.getKeyProperty("name"); + if (nameTagValue != null) { + return Tags.of("name", nameTagValue.replaceAll("\"", "")); + } + return Collections.emptyList(); + } + + @Override + public void close() { + for (NotificationListener notificationListener : this.notificationListeners) { + try { + this.mBeanServer.removeNotificationListener(MBeanServerDelegate.DELEGATE_NAME, notificationListener); + } catch (InstanceNotFoundException | ListenerNotFoundException ex) { + throw new RuntimeException(ex); + } + } + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/Issue.java b/micrometer-binders/src/test/java/io/micrometer/binder/Issue.java new file mode 100644 index 0000000000..364f0bdc64 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/Issue.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 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.binder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a test as related to a Github issue. + * + * @author Jon Schneider + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface Issue { + String value(); +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/AbstractCacheMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/AbstractCacheMetricsTest.java new file mode 100644 index 0000000000..9f4a246fb5 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/AbstractCacheMetricsTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 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.binder.cache; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.cache.CacheMeterBinder; +import io.micrometer.core.instrument.search.RequiredSearch; + +/** + * Base class for cache metrics tests. + * + * @author Oleksii Bondar + */ +abstract class AbstractCacheMetricsTest { + + protected Tags expectedTag = Tags.of("app", "test"); + + /** + * Verifies base metrics presence + */ + protected void verifyCommonCacheMetrics(MeterRegistry meterRegistry, CacheMeterBinder meterBinder) { + meterRegistry.get("cache.puts").tags(expectedTag).functionCounter(); + meterRegistry.get("cache.gets").tags(expectedTag).tag("result", "hit").functionCounter(); + + if (meterBinder.size() != null) { + meterRegistry.get("cache.size").tags(expectedTag).gauge(); + } + if (meterBinder.missCount() != null) { + meterRegistry.get("cache.gets").tags(expectedTag).tag("result", "miss").functionCounter(); + } + if (meterBinder.evictionCount() != null) { + meterRegistry.get("cache.evictions").tags(expectedTag).functionCounter(); + } + } + + protected RequiredSearch fetch(MeterRegistry meterRegistry, String name) { + return fetch(meterRegistry, name, expectedTag); + } + + protected RequiredSearch fetch(MeterRegistry meterRegistry, String name, Iterable tags) { + return meterRegistry.get(name).tags(tags); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsTest.java new file mode 100644 index 0000000000..3bbce625ac --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018 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.binder.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.github.benmanes.caffeine.cache.stats.CacheStats; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.binder.cache.CaffeineCacheMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.cache.CaffeineCacheMetrics}. + * + * @author Oleksii Bondar + */ +class CaffeineCacheMetricsTest extends AbstractCacheMetricsTest { + + private LoadingCache cache = Caffeine.newBuilder().build(key -> ""); + private io.micrometer.binder.cache.CaffeineCacheMetrics> metrics = new io.micrometer.binder.cache.CaffeineCacheMetrics<>(cache, "testCache", expectedTag); + + @Test + void reportExpectedGeneralMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + FunctionCounter evictionWeight = fetch(registry, "cache.eviction.weight").functionCounter(); + CacheStats stats = cache.stats(); + assertThat(evictionWeight.count()).isEqualTo(stats.evictionWeight()); + + // specific to LoadingCache instance + TimeGauge loadDuration = fetch(registry, "cache.load.duration").timeGauge(); + assertThat(loadDuration.value()).isEqualTo(stats.totalLoadTime()); + + FunctionCounter successfulLoad = fetch(registry, "cache.load", Tags.of("result", "success")).functionCounter(); + assertThat(successfulLoad.count()).isEqualTo(stats.loadSuccessCount()); + + FunctionCounter failedLoad = fetch(registry, "cache.load", Tags.of("result", "failure")).functionCounter(); + assertThat(failedLoad.count()).isEqualTo(stats.loadFailureCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.CaffeineCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag); + + meterRegistry.get("cache.eviction.weight").tags(expectedTag).functionCounter(); + } + + @Test + void doNotReportMetricsForNonLoadingCache() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + Cache cache = Caffeine.newBuilder().build(); + io.micrometer.binder.cache.CaffeineCacheMetrics> metrics = new CaffeineCacheMetrics<>(cache, "testCache", expectedTag); + metrics.bindTo(meterRegistry); + + assertThat(meterRegistry.find("cache.load.duration").timeGauge()).isNull(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.estimatedSize()); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.stats().hitCount()); + } + + @Test + void returnMissCount() { + assertThat(metrics.missCount()).isEqualTo(cache.stats().missCount()); + } + + @Test + void returnEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(cache.stats().evictionCount()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.stats().loadCount()); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineStatsCounterTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineStatsCounterTest.java new file mode 100644 index 0000000000..3867cb4aa7 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/CaffeineStatsCounterTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2021 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.binder.cache; + +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.binder.cache.CaffeineStatsCounter; +import io.micrometer.core.instrument.search.RequiredSearch; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.cache.CaffeineStatsCounter}. + * + * @author John Karp + */ +class CaffeineStatsCounterTest { + private static final String CACHE_NAME = "foo"; + private static final Tags USER_TAGS = Tags.of("k", "v"); + private static final Tags TAGS = Tags.concat(USER_TAGS, "cache", CACHE_NAME); + + private io.micrometer.binder.cache.CaffeineStatsCounter stats; + private MeterRegistry registry; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + stats = new CaffeineStatsCounter(registry, CACHE_NAME, USER_TAGS); + } + + @Test + void registerSize() { + Cache cache = Caffeine.newBuilder().maximumSize(10).recordStats(() -> stats).build(); + stats.registerSizeMetric(cache); + assertThat(fetch("cache.size").gauge().value()).isEqualTo(0); + cache.put("foo", "bar"); + assertThat(fetch("cache.size").gauge().value()).isEqualTo(1); + } + + @Test + void hit() { + stats.recordHits(2); + assertThat(fetch("cache.gets", "result", "hit").counter().count()).isEqualTo(2); + } + + @Test + void miss() { + stats.recordMisses(2); + assertThat(fetch("cache.gets", "result", "miss").counter().count()).isEqualTo(2); + } + + @Test + void loadSuccess() { + stats.recordLoadSuccess(256); + Timer timer = fetch("cache.loads", "result", "success").timer(); + assertThat(timer.count()).isEqualTo(1); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isEqualTo(256); + } + + @Test + void loadFailure() { + stats.recordLoadFailure(256); + Timer timer = fetch("cache.loads", "result", "failure").timer(); + assertThat(timer.count()).isEqualTo(1); + assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isEqualTo(256); + } + + @ParameterizedTest + @EnumSource(RemovalCause.class) + void evictionWithCause(RemovalCause cause) { + stats.recordEviction(3, cause); + DistributionSummary summary = fetch("cache.evictions", "cause", cause.name()).summary(); + assertThat(summary.count()).isEqualTo(1); + assertThat(summary.totalAmount()).isEqualTo(3); + } + + private RequiredSearch fetch(String name, String... tags) { + return registry.get(name).tags(TAGS).tags(tags); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/EhCache2MetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/EhCache2MetricsTest.java new file mode 100644 index 0000000000..2ec8116447 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/EhCache2MetricsTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2018 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.binder.cache; + +import java.util.Random; + +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.cache.EhCache2Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.statistics.StatisticsGateway; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.cache.EhCache2Metrics}. + * + * @author Oleksii Bondar + */ +class EhCache2MetricsTest extends AbstractCacheMetricsTest { + + private static CacheManager cacheManager; + private static Cache cache; + + private io.micrometer.binder.cache.EhCache2Metrics metrics = new io.micrometer.binder.cache.EhCache2Metrics(cache, expectedTag); + + @Test + void reportMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + StatisticsGateway stats = cache.getStatistics(); + + Gauge remoteSize = fetch(registry, "cache.remoteSize").gauge(); + assertThat(remoteSize.value()).isEqualTo(stats.getRemoteSize()); + + FunctionCounter cacheRemovals = fetch(registry, "cache.removals").functionCounter(); + assertThat(cacheRemovals.count()).isEqualTo(stats.cacheRemoveCount()); + + String cacheAdded = "cache.puts.added"; + FunctionCounter putsAdded = fetch(registry, cacheAdded, Tags.of("result", "added")).functionCounter(); + assertThat(putsAdded.count()).isEqualTo(stats.cachePutAddedCount()); + + FunctionCounter putsUpdated = fetch(registry, cacheAdded, Tags.of("result", "updated")).functionCounter(); + assertThat(putsUpdated.count()).isEqualTo(stats.cachePutUpdatedCount()); + + Gauge offHeapSize = fetch(registry, "cache.local.offheap.size").gauge(); + assertThat(offHeapSize.value()).isEqualTo(stats.getLocalOffHeapSizeInBytes()); + + Gauge heapSize = fetch(registry, "cache.local.heap.size").gauge(); + assertThat(heapSize.value()).isEqualTo(stats.getLocalHeapSizeInBytes()); + + Gauge diskSize = fetch(registry, "cache.local.disk.size").gauge(); + assertThat(diskSize.value()).isEqualTo(stats.getLocalDiskSizeInBytes()); + + // miss metrics + String misses = "cache.misses"; + FunctionCounter expiredMisses = fetch(registry, misses, Tags.of("reason", "expired")).functionCounter(); + assertThat(expiredMisses.count()).isEqualTo(stats.cacheMissExpiredCount()); + + FunctionCounter notFoundMisses = fetch(registry, misses, Tags.of("reason", "notFound")).functionCounter(); + assertThat(notFoundMisses.count()).isEqualTo(stats.cacheMissNotFoundCount()); + + // commit transaction metrics + String xaCommits = "cache.xa.commits"; + FunctionCounter readOnlyCommits = fetch(registry, xaCommits, Tags.of("result", "readOnly")).functionCounter(); + assertThat(readOnlyCommits.count()).isEqualTo(stats.xaCommitReadOnlyCount()); + + FunctionCounter exceptionCommits = fetch(registry, xaCommits, Tags.of("result", "exception")).functionCounter(); + assertThat(exceptionCommits.count()).isEqualTo(stats.xaCommitExceptionCount()); + + FunctionCounter committedCommits = fetch(registry, xaCommits, Tags.of("result", "committed")).functionCounter(); + assertThat(committedCommits.count()).isEqualTo(stats.xaCommitCommittedCount()); + + // rollback transaction metrics + String xaRollbacks = "cache.xa.rollbacks"; + FunctionCounter exceptionRollback = fetch(registry, xaRollbacks, Tags.of("result", "exception")) + .functionCounter(); + assertThat(exceptionRollback.count()).isEqualTo(stats.xaRollbackExceptionCount()); + + FunctionCounter successRollback = fetch(registry, xaRollbacks, Tags.of("result", "success")).functionCounter(); + assertThat(successRollback.count()).isEqualTo(stats.xaRollbackSuccessCount()); + + // recovery transaction metrics + String xaRecoveries = "cache.xa.recoveries"; + FunctionCounter nothingRecovered = fetch(registry, xaRecoveries, Tags.of("result", "nothing")) + .functionCounter(); + assertThat(nothingRecovered.count()).isEqualTo(stats.xaRecoveryNothingCount()); + + FunctionCounter successRecoveries = fetch(registry, xaRecoveries, Tags.of("result", "success")) + .functionCounter(); + assertThat(successRecoveries.count()).isEqualTo(stats.xaRecoveryRecoveredCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + EhCache2Metrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.remoteSize").tags(expectedTag).gauge(); + } + + @Test + void returnCacheSize() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.size()).isEqualTo(stats.getSize()); + } + + @Test + void returnEvictionCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.evictionCount()).isEqualTo(stats.cacheEvictedCount()); + } + + @Test + void returnHitCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.hitCount()).isEqualTo(stats.cacheHitCount()); + } + + @Test + void returnMissCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.missCount()).isEqualTo(stats.cacheMissCount()); + } + + @Test + void returnPutCount() { + StatisticsGateway stats = cache.getStatistics(); + assertThat(metrics.putCount()).isEqualTo(stats.cachePutCount()); + } + + @BeforeAll + static void setup() { + cacheManager = CacheManager.newInstance(); + cacheManager.addCache("testCache"); + cache = spy(cacheManager.getCache("testCache")); + StatisticsGateway stats = mock(StatisticsGateway.class); + // generate non-negative random value to address false-positives + int valueBound = 100000; + Random random = new Random(); + when(stats.getSize()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheEvictedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheHitCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getRemoteSize()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheRemoveCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutAddedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cachePutUpdatedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalOffHeapSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalHeapSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.getLocalDiskSizeInBytes()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissExpiredCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.cacheMissNotFoundCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitCommittedCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitExceptionCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaCommitReadOnlyCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRollbackExceptionCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRollbackSuccessCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRecoveryRecoveredCount()).thenReturn((long) random.nextInt(valueBound)); + when(stats.xaRecoveryNothingCount()).thenReturn((long) random.nextInt(valueBound)); + when(cache.getStatistics()).thenReturn(stats); + } + + @AfterAll + static void cleanup() { + cacheManager.removeAllCaches(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsTest.java new file mode 100644 index 0000000000..03d378caf6 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 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.binder.cache; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.CacheStats; +import com.google.common.cache.LoadingCache; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.TimeGauge; +import io.micrometer.binder.cache.GuavaCacheMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.cache.GuavaCacheMetrics}. + * + * @author Oleksii Bondar + */ +class GuavaCacheMetricsTest extends AbstractCacheMetricsTest { + + private LoadingCache cache = CacheBuilder.newBuilder().build(new CacheLoader() { + public String load(String key) throws Exception { + return ""; + } + }); + private io.micrometer.binder.cache.GuavaCacheMetrics> metrics = new io.micrometer.binder.cache.GuavaCacheMetrics<>(cache, "testCache", expectedTag); + + @Test + void reportExpectedMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindTo(registry); + + verifyCommonCacheMetrics(registry, metrics); + + // common metrics + Gauge cacheSize = fetch(registry, "cache.size").gauge(); + assertThat(cacheSize.value()).isEqualTo(cache.size()); + + FunctionCounter hitCount = fetch(registry, "cache.gets", Tags.of("result", "hit")).functionCounter(); + assertThat(hitCount.count()).isEqualTo(metrics.hitCount()); + + FunctionCounter missCount = fetch(registry, "cache.gets", Tags.of("result", "miss")).functionCounter(); + assertThat(missCount.count()).isEqualTo(metrics.missCount().doubleValue()); + + FunctionCounter cachePuts = fetch(registry, "cache.puts").functionCounter(); + assertThat(cachePuts.count()).isEqualTo(metrics.putCount()); + + FunctionCounter cacheEviction = fetch(registry, "cache.evictions").functionCounter(); + assertThat(cacheEviction.count()).isEqualTo(metrics.evictionCount().doubleValue()); + + CacheStats stats = cache.stats(); + TimeGauge loadDuration = fetch(registry, "cache.load.duration").timeGauge(); + assertThat(loadDuration.value()).isEqualTo(stats.totalLoadTime()); + + FunctionCounter successfulLoad = fetch(registry, "cache.load", Tags.of("result", "success")).functionCounter(); + assertThat(successfulLoad.count()).isEqualTo(stats.loadSuccessCount()); + + FunctionCounter failedLoad = fetch(registry, "cache.load", Tags.of("result", "failure")).functionCounter(); + assertThat(failedLoad.count()).isEqualTo(stats.loadExceptionCount()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + GuavaCacheMetrics.monitor(meterRegistry, cache, "testCache", expectedTag); + + meterRegistry.get("cache.load.duration").tags(expectedTag).timeGauge(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.size()); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.stats().hitCount()); + } + + @Test + void returnMissCount() { + assertThat(metrics.missCount()).isEqualTo(cache.stats().missCount()); + } + + @Test + void returnEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(cache.stats().evictionCount()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.stats().loadCount()); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsTest.java new file mode 100644 index 0000000000..599c165e7c --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsTest.java @@ -0,0 +1,191 @@ +/* + * Copyright 2018 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.binder.cache; + +import java.util.HashMap; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import com.hazelcast.core.Hazelcast; +import com.hazelcast.internal.monitor.impl.LocalMapStatsImpl; +import com.hazelcast.internal.monitor.impl.NearCacheStatsImpl; +import com.hazelcast.map.IMap; +import com.hazelcast.map.LocalMapStats; +import com.hazelcast.nearcache.NearCacheStats; +import io.micrometer.core.instrument.FunctionCounter; +import io.micrometer.core.instrument.FunctionTimer; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.cache.HazelcastCacheMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.cache.HazelcastCacheMetrics}. + * + * @author Oleksii Bondar + */ +class HazelcastCacheMetricsTest extends AbstractCacheMetricsTest { + + private static IMap cache; + + private io.micrometer.binder.cache.HazelcastCacheMetrics metrics = new io.micrometer.binder.cache.HazelcastCacheMetrics(cache, expectedTag); + + @Test + void reportMetrics() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + metrics.bindTo(meterRegistry); + + verifyCommonCacheMetrics(meterRegistry, metrics); + + LocalMapStats localMapStats = cache.getLocalMapStats(); + + Gauge backupEntries = fetch(meterRegistry, "cache.entries", Tags.of("ownership", "backup")).gauge(); + assertThat(backupEntries.value()).isEqualTo(localMapStats.getBackupEntryCount()); + + Gauge ownedEntries = fetch(meterRegistry, "cache.entries", Tags.of("ownership", "owned")).gauge(); + assertThat(ownedEntries.value()).isEqualTo(localMapStats.getOwnedEntryCount()); + + Gauge backupEntryMemory = fetch(meterRegistry, "cache.entry.memory", Tags.of("ownership", "backup")).gauge(); + assertThat(backupEntryMemory.value()).isEqualTo(localMapStats.getBackupEntryMemoryCost()); + + Gauge ownedEntryMemory = fetch(meterRegistry, "cache.entry.memory", Tags.of("ownership", "owned")).gauge(); + assertThat(ownedEntryMemory.value()).isEqualTo(localMapStats.getOwnedEntryMemoryCost()); + + FunctionCounter partitionGets = fetch(meterRegistry, "cache.partition.gets").functionCounter(); + assertThat(partitionGets.count()).isEqualTo(localMapStats.getGetOperationCount()); + + // near cache stats + NearCacheStats nearCacheStats = localMapStats.getNearCacheStats(); + Gauge hitCacheRequests = fetch(meterRegistry, "cache.near.requests", Tags.of("result", "hit")).gauge(); + assertThat(hitCacheRequests.value()).isEqualTo(nearCacheStats.getHits()); + + Gauge missCacheRequests = fetch(meterRegistry, "cache.near.requests", Tags.of("result", "miss")).gauge(); + assertThat(missCacheRequests.value()).isEqualTo(nearCacheStats.getMisses()); + + Gauge nearPersistance = fetch(meterRegistry, "cache.near.persistences").gauge(); + assertThat(nearPersistance.value()).isEqualTo(nearCacheStats.getPersistenceCount()); + + Gauge nearEvictions = fetch(meterRegistry, "cache.near.evictions").gauge(); + assertThat(nearEvictions.value()).isEqualTo(nearCacheStats.getEvictions()); + + // timings + TimeUnit timeUnit = TimeUnit.MILLISECONDS; + FunctionTimer getsLatency = fetch(meterRegistry, "cache.gets.latency").functionTimer(); + assertThat(getsLatency.count()).isEqualTo(localMapStats.getGetOperationCount()); + assertThat(getsLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalGetLatency()); + + FunctionTimer putsLatency = fetch(meterRegistry, "cache.puts.latency").functionTimer(); + assertThat(putsLatency.count()).isEqualTo(localMapStats.getPutOperationCount()); + assertThat(putsLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalPutLatency()); + + FunctionTimer removeLatency = fetch(meterRegistry, "cache.removals.latency").functionTimer(); + assertThat(removeLatency.count()).isEqualTo(localMapStats.getRemoveOperationCount()); + assertThat(removeLatency.totalTime(timeUnit)).isEqualTo(localMapStats.getTotalRemoveLatency()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.HazelcastCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.partition.gets").tags(expectedTag).functionCounter(); + } + + @Test + void doNotReportEvictionCountSinceNotImplemented() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.HazelcastCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + assertThat(meterRegistry.find("cache.evictions").functionCounter()).isNull(); + } + + @Test + void doNotReportMissCountSinceNotImplemented() { + MeterRegistry registry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.HazelcastCacheMetrics.monitor(registry, cache, expectedTag); + + assertThat(registry.find("cache.gets").tags(Tags.of("result", "miss")).functionCounter()).isNull(); + } + + @Test + void returnCacheSize() { + assertThat(metrics.size()).isEqualTo(cache.size()); + } + + @Test + void returnNullForMissCount() { + assertThat(metrics.missCount()).isNull(); + } + + @Test + void returnNullForEvictionCount() { + assertThat(metrics.evictionCount()).isNull(); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(cache.getLocalMapStats().getHits()); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(cache.getLocalMapStats().getPutOperationCount()); + } + + @Test + void nonIMapCacheFails() { + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> new HazelcastCacheMetrics(new HashMap(), Tags.empty())); + } + + @BeforeAll + static void setup() { + cache = Hazelcast.newHazelcastInstance().getMap("mycache"); + NearCacheStats nearCacheStats = mock(NearCacheStatsImpl.class); + // generate non-negative random value to address false-positives + int valueBound = 100000; + Random random = new Random(); + when(nearCacheStats.getMisses()).thenReturn((long) random.nextInt(valueBound)); + when(nearCacheStats.getPersistenceCount()).thenReturn((long) random.nextInt(valueBound)); + when(nearCacheStats.getEvictions()).thenReturn((long) random.nextInt(valueBound)); + + LocalMapStatsImpl localMapStats = (LocalMapStatsImpl) cache.getLocalMapStats(); + localMapStats.setNearCacheStats(nearCacheStats); + + localMapStats.setBackupEntryCount(random.nextInt(valueBound)); + localMapStats.setBackupEntryMemoryCost(random.nextInt(valueBound)); + localMapStats.setOwnedEntryCount(random.nextInt(valueBound)); + localMapStats.setOwnedEntryMemoryCost(random.nextInt(valueBound)); + localMapStats.incrementGetLatencyNanos(random.nextInt(valueBound)); + localMapStats.incrementPutLatencyNanos(random.nextInt(valueBound)); + localMapStats.incrementRemoveLatencyNanos(random.nextInt(valueBound)); + } + + @AfterAll + static void cleanup() { + Hazelcast.shutdownAll(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/cache/JCacheMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/cache/JCacheMetricsTest.java new file mode 100644 index 0000000000..c6a7c1ae4b --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/cache/JCacheMetricsTest.java @@ -0,0 +1,208 @@ +/* + * Copyright 2018 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.binder.cache; + +import java.net.URI; +import java.util.Random; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.AttributeNotFoundException; +import javax.management.DynamicMBean; +import javax.management.InvalidAttributeValueException; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanServer; +import javax.management.MBeanServerFactory; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; +import javax.management.ReflectionException; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.cache.JCacheMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.cache.JCacheMetrics}. + * + * @author Oleksii Bondar + */ +class JCacheMetricsTest extends AbstractCacheMetricsTest { + + @SuppressWarnings("unchecked") + private Cache cache = mock(Cache.class); + + private CacheManager cacheManager = mock(CacheManager.class); + + private io.micrometer.binder.cache.JCacheMetrics> metrics; + private MBeanServer mbeanServer; + private Long expectedAttributeValue = new Random().nextLong(); + + @BeforeEach + void setup() throws Exception { + when(cache.getCacheManager()).thenReturn(cacheManager); + when(cache.getName()).thenReturn("testCache"); + when(cacheManager.getURI()).thenReturn(new URI("http://localhost")); + metrics = new io.micrometer.binder.cache.JCacheMetrics<>(cache, expectedTag); + + // emulate MBean server with MBean used for statistic lookup + mbeanServer = MBeanServerFactory.createMBeanServer(); + ObjectName objectName = new ObjectName("javax.cache:type=CacheStatistics"); + metrics.objectName = objectName; + CacheMBeanStub mBean = new CacheMBeanStub(expectedAttributeValue); + mbeanServer.registerMBean(mBean, objectName); + } + + @AfterEach + void tearDown() { + if (mbeanServer != null) { + MBeanServerFactory.releaseMBeanServer(mbeanServer); + } + } + + @Test + void reportExpectedMetrics() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + metrics.bindTo(meterRegistry); + + verifyCommonCacheMetrics(meterRegistry, metrics); + + Gauge cacheRemovals = fetch(meterRegistry, "cache.removals").gauge(); + assertThat(cacheRemovals.value()).isEqualTo(expectedAttributeValue.doubleValue()); + } + + @Test + void constructInstanceViaStaticMethodMonitor() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.JCacheMetrics.monitor(meterRegistry, cache, expectedTag); + + meterRegistry.get("cache.removals").tags(expectedTag).gauge(); + } + + @Test + void constructInstanceViaStaticMethodMonitorWithVarArgTags() { + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + io.micrometer.binder.cache.JCacheMetrics.monitor(meterRegistry, cache, "version", "1.0"); + + meterRegistry.get("cache.removals").tags(Tags.of("version", "1.0")).gauge(); + } + + @Test + void returnNullForCacheSize() { + assertThat(metrics.size()).isNull(); + } + + @Test + void returnMissCount() { + assertThat(metrics.missCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnEvictionCount() { + assertThat(metrics.evictionCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnHitCount() { + assertThat(metrics.hitCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void returnPutCount() { + assertThat(metrics.putCount()).isEqualTo(expectedAttributeValue); + } + + @Test + void defaultValueWhenNoMBeanAttributeFound() throws MalformedObjectNameException { + // change source MBean to emulate AttributeNotFoundException + metrics.objectName = new ObjectName("javax.cache:type=CacheInformation"); + + assertThat(metrics.hitCount()).isEqualTo(0L); + } + + @Test + void defaultValueWhenObjectNameNotInitialized() throws MalformedObjectNameException { + // set cacheManager to null to emulate scenario when objectName not initialized + when(cache.getCacheManager()).thenReturn(null); + metrics = new io.micrometer.binder.cache.JCacheMetrics<>(cache, expectedTag); + + assertThat(metrics.hitCount()).isEqualTo(0L); + } + + @Test + void doNotReportMetricWhenObjectNameNotInitialized() throws MalformedObjectNameException { + // set cacheManager to null to emulate scenario when objectName not initialized + when(cache.getCacheManager()).thenReturn(null); + metrics = new JCacheMetrics<>(cache, expectedTag); + MeterRegistry registry = new SimpleMeterRegistry(); + metrics.bindImplementationSpecificMetrics(registry); + + assertThat(registry.find("cache.removals").tags(expectedTag).functionCounter()).isNull(); + } + + private static class CacheMBeanStub implements DynamicMBean { + + private Long expectedAttributeValue; + + public CacheMBeanStub(Long attributeValue) { + this.expectedAttributeValue = attributeValue; + } + + @Override + public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { + return expectedAttributeValue; + } + + @Override + public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { + } + + @Override + public AttributeList getAttributes(String[] attributes) { + return null; + } + + @Override + public AttributeList setAttributes(AttributeList attributes) { + return null; + } + + @Override + public Object invoke(String actionName, + Object[] params, + String[] signature) throws MBeanException, ReflectionException { + return null; + } + + @Override + public MBeanInfo getMBeanInfo() { + return new MBeanInfo(CacheMBeanStub.class.getName(), "description", null, null, null, null); + } + + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/commonspool2/CommonsObjectPool2MetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/commonspool2/CommonsObjectPool2MetricsTest.java new file mode 100644 index 0000000000..cf48876142 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/commonspool2/CommonsObjectPool2MetricsTest.java @@ -0,0 +1,193 @@ +/* + * Copyright 2020 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.binder.commonspool2; + +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.commonspool2.CommonsObjectPool2Metrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.commons.pool2.BaseKeyedPooledObjectFactory; +import org.apache.commons.pool2.BasePooledObjectFactory; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.impl.DefaultPooledObject; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Chao Chang + */ +class CommonsObjectPool2MetricsTest { + private int genericObjectPoolCount = 0; + + private Tags tags = Tags.of("app", "myapp", "version", "1"); + private final MeterRegistry registry = new SimpleMeterRegistry(); + private final io.micrometer.binder.commonspool2.CommonsObjectPool2Metrics commonsObjectPool2Metrics = new CommonsObjectPool2Metrics(tags); + + @AfterEach + void afterEach() { + commonsObjectPool2Metrics.close(); + } + + @Test + void verifyMetricsWithExpectedTags() { + try (GenericObjectPool p = createGenericObjectPool()) { + MeterRegistry registry = new SimpleMeterRegistry(); + commonsObjectPool2Metrics.bindTo(registry); + + registry.get("commons.pool2.num.idle").tags(tags).gauge(); + registry.get("commons.pool2.num.waiters").tags(tags).gauge(); + + Arrays.asList( + "commons.pool2.created", + "commons.pool2.borrowed", + "commons.pool2.returned", + "commons.pool2.destroyed", + "commons.pool2.destroyed.by.evictor", + "commons.pool2.destroyed.by.borrow.validation" + ).forEach(name -> registry.get(name).tags(tags).functionCounter()); + + Arrays.asList( + "commons.pool2.max.borrow.wait", + "commons.pool2.mean.active", + "commons.pool2.mean.idle", + "commons.pool2.mean.borrow.wait" + ).forEach(name -> registry.get(name).tags(tags).timeGauge()); + } + } + + @Test + void verifyGenericKeyedObjectPoolMetricsWithExpectedTags() { + try (GenericKeyedObjectPool p = createGenericKeyedObjectPool()) { + Tags tagsToMatch = tags.and("type", "GenericKeyedObjectPool"); + commonsObjectPool2Metrics.bindTo(registry); + + Arrays.asList( + "commons.pool2.num.idle", + "commons.pool2.num.waiters" + ).forEach(name -> registry.get(name).tags(tagsToMatch).gauge()); + + Arrays.asList( + "commons.pool2.created", + "commons.pool2.borrowed", + "commons.pool2.returned", + "commons.pool2.destroyed", + "commons.pool2.destroyed.by.evictor", + "commons.pool2.destroyed.by.borrow.validation" + ).forEach(name -> registry.get(name).tags(tagsToMatch).functionCounter()); + + Arrays.asList( + "commons.pool2.max.borrow.wait", + "commons.pool2.mean.active", + "commons.pool2.mean.idle", + "commons.pool2.mean.borrow.wait" + ).forEach(name -> registry.get(name).tags(tagsToMatch).timeGauge()); + } + } + + @Test + void verifyIdleAndActiveInstancesAreReported() throws Exception { + commonsObjectPool2Metrics.bindTo(registry); + CountDownLatch latch = new CountDownLatch(1); + registry.config().onMeterAdded(m -> { + if (m.getId().getName().contains("commons.pool2")) + latch.countDown(); + }); + + try (GenericObjectPool genericObjectPool = createGenericObjectPool()) { + latch.await(10, TimeUnit.SECONDS); + final Object o = genericObjectPool.borrowObject(10_000L); + + assertThat(registry.get("commons.pool2.num.active").gauge().value()).isEqualTo(1.0); + assertThat(registry.get("commons.pool2.num.idle").gauge().value()).isEqualTo(0.0); + + genericObjectPool.returnObject(o); + + assertThat(registry.get("commons.pool2.num.active").gauge().value()).isEqualTo(0.0); + assertThat(registry.get("commons.pool2.num.idle").gauge().value()).isEqualTo(1.0); + } + } + + @Test + void metricsReportedPerMultiplePools() { + try (final GenericObjectPool p1 = createGenericObjectPool(); + final GenericObjectPool p2 = createGenericObjectPool(); + final GenericObjectPool p3 = createGenericObjectPool()) { + + MeterRegistry registry = new SimpleMeterRegistry(); + commonsObjectPool2Metrics.bindTo(registry); + + registry.get("commons.pool2.num.waiters").tag("name", "pool" + genericObjectPoolCount).gauge(); + registry.get("commons.pool2.num.waiters").tag("name", "pool" + (genericObjectPoolCount - 1)).gauge(); + } + } + + @Test + void newPoolsAreDiscoveredByListener() throws InterruptedException { + MeterRegistry registry = new SimpleMeterRegistry(); + commonsObjectPool2Metrics.bindTo(registry); + + CountDownLatch latch = new CountDownLatch(1); + registry.config().onMeterAdded(m -> { + if (m.getId().getName().contains("commons.pool2")) + latch.countDown(); + }); + + try (GenericObjectPool p = createGenericObjectPool()) { + latch.await(10, TimeUnit.SECONDS); + } + } + + private GenericObjectPool createGenericObjectPool() { + genericObjectPoolCount++; + GenericObjectPoolConfig config = new GenericObjectPoolConfig<>(); + config.setMaxTotal(10); + + return new GenericObjectPool<>(new BasePooledObjectFactory() { + @Override + public Object create() { + return new Object(); + } + + @Override + public PooledObject wrap(Object testObject) { + return new DefaultPooledObject<>(testObject); + } + }, config); + } + + private GenericKeyedObjectPool createGenericKeyedObjectPool() { + return new GenericKeyedObjectPool<>(new BaseKeyedPooledObjectFactory() { + @Override + public Object create(Object key) { + return key; + } + + @Override + public PooledObject wrap(Object value) { + return new DefaultPooledObject<>(value); + } + }); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/db/DatabaseTableMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/db/DatabaseTableMetricsTest.java new file mode 100644 index 0000000000..80adc55924 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/db/DatabaseTableMetricsTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 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.binder.db; + +import java.sql.Connection; +import java.sql.SQLException; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.db.DatabaseTableMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.hsqldb.jdbc.JDBCDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jon Schneider + */ +class DatabaseTableMetricsTest { + private MeterRegistry registry; + private JDBCDataSource ds; + + @BeforeEach + void setup() throws SQLException { + registry = new SimpleMeterRegistry(); + + ds = new JDBCDataSource(); + ds.setURL("jdbc:hsqldb:mem:test"); + + try (Connection conn = ds.getConnection()) { + conn.prepareStatement("CREATE TABLE foo (id int)").execute(); + conn.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate(); + } + } + + @AfterEach + void shutdown() throws SQLException { + try (Connection conn = ds.getConnection()) { + conn.prepareStatement("SHUTDOWN").execute(); + } + } + + @Test + void rowCountGauge() { + io.micrometer.binder.db.DatabaseTableMetrics.monitor(registry, "foo", "mydb", ds); + assertThat(registry.get("db.table.size") + .tag("table", "foo") + .tag("db", "mydb") + .gauge().value()).isEqualTo(1.0); + } + + @Test + void rowCountForNonExistentTable() { + DatabaseTableMetrics.monitor(registry, "dne", "mydb", ds); + assertThat(registry.get("db.table.size") + .tag("table", "dne") + .tag("db", "mydb") + .gauge().value()).isEqualTo(0.0); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/db/MetricsDSLContextTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/db/MetricsDSLContextTest.java new file mode 100644 index 0000000000..8d0fd6b95f --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/db/MetricsDSLContextTest.java @@ -0,0 +1,237 @@ +/* + * Copyright 2020 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.binder.db; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.db.MetricsDSLContext; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.lang.NonNull; +import org.jooq.Configuration; +import org.jooq.ExecuteListener; +import org.jooq.ExecuteListenerProvider; +import org.jooq.Record; +import org.jooq.Record1; +import org.jooq.Result; +import org.jooq.SQLDialect; +import org.jooq.SelectJoinStep; +import org.jooq.SelectSelectStep; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; +import org.jooq.impl.DefaultConfiguration; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.db.MetricsDSLContext.withMetrics; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.jooq.impl.DSL.asterisk; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.table; +import static org.jooq.impl.DSL.using; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link io.micrometer.binder.db.MetricsDSLContext}. + * + * @author Jon Schneider + * @author Johnny Lim + */ +class MetricsDSLContextTest { + private MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @Test + void timeFluentSelectStatement() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + Result result = jooq.tag("name", "selectAllAuthors").select(asterisk()).from("author").fetch(); + assertThat(result.size()).isEqualTo(1); + + // intentionally don't time this operation to demonstrate that the configuration hasn't been globally mutated + jooq.select(asterisk()).from("author").fetch(); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors") + .tag("type", "read") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void timeParsedSelectStatement() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:parsedSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + jooq.tag("name", "selectAllAuthors").fetch("SELECT * FROM author"); + + // intentionally don't time this operation to demonstrate that the configuration hasn't been globally mutated + jooq.fetch("SELECT * FROM author"); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors") + .tag("type", "read") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void timeFaultySelectStatement() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:faultySelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + jooq.tag("name", "selectAllAuthors").fetch("SELECT non_existent_field FROM author"); + + failBecauseExceptionWasNotThrown(DataAccessException.class); + } catch (DataAccessException ignored) { + } + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors") + .tag("type", "read") + .tag("exception", "c42 syntax error or access rule violation") + .tag("exception.subclass", "none") + .timer().count()) + .isEqualTo(1); + } + + @Test + void timeExecute() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + jooq.tag("name", "selectAllAuthors").execute("SELECT * FROM author"); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void timeInsert() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + jooq.tag("name", "insertAuthor").insertInto(table("author")).values("2", "jon", "schneider") + .execute(); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "insertAuthor") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void timeUpdate() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + jooq.tag("name", "updateAuthor").update(table("author")).set(field("author.first_name"), "jon") + .execute(); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "updateAuthor") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void timingBatchQueriesNotSupported() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + jooq.tag("name", "batch").batch( + jooq.insertInto(table("author")).values("3", "jon", "schneider"), + jooq.insertInto(table("author")).values("4", "jon", "schneider") + ).execute(); + + assertThat(meterRegistry.find("jooq.query").timer()).isNull(); + } + } + + /** + * Ensures that we are holding tag state in a way that doesn't conflict between two unexecuted queries. + */ + @Test + void timeTwoStatementsCreatedBeforeEitherIsExecuted() throws SQLException { + try (Connection conn = DriverManager.getConnection("jdbc:h2:mem:fluentSelect")) { + io.micrometer.binder.db.MetricsDSLContext jooq = createDatabase(conn); + + SelectJoinStep select1 = jooq.tag("name", "selectAllAuthors").select(asterisk()).from("author"); + SelectJoinStep> select2 = jooq.tag("name", "selectAllAuthors2") + .select(field("first_name")).from("author"); + + select1.fetch(); + select2.fetch(); + select1.fetch(); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors") + .timer().count()) + .isEqualTo(2); + + assertThat(meterRegistry.get("jooq.query") + .tag("name", "selectAllAuthors2") + .timer().count()) + .isEqualTo(1); + } + } + + @Test + void userExecuteListenerShouldBePreserved() { + ExecuteListener userExecuteListener = mock(ExecuteListener.class); + Configuration configuration = new DefaultConfiguration().set(() -> userExecuteListener); + + io.micrometer.binder.db.MetricsDSLContext jooq = withMetrics(using(configuration), meterRegistry, Tags.empty()); + + ExecuteListenerProvider[] executeListenerProviders = jooq.configuration().executeListenerProviders(); + assertThat(executeListenerProviders).hasSize(2); + assertThat(executeListenerProviders[0].provide()).isSameAs(userExecuteListener); + assertThat(executeListenerProviders[1].provide()).isInstanceOf(io.micrometer.binder.db.JooqExecuteListener.class); + + SelectSelectStep select = jooq.tag("name", "selectAllAuthors").select(asterisk()); + executeListenerProviders = select.configuration().executeListenerProviders(); + assertThat(executeListenerProviders).hasSize(2); + assertThat(executeListenerProviders[0].provide()).isSameAs(userExecuteListener); + assertThat(executeListenerProviders[1].provide()).isInstanceOf(io.micrometer.binder.db.JooqExecuteListener.class); + } + + @NonNull + private io.micrometer.binder.db.MetricsDSLContext createDatabase(Connection conn) { + Configuration configuration = new DefaultConfiguration() + .set(conn) + .set(SQLDialect.H2); + + MetricsDSLContext jooq = withMetrics(DSL.using(configuration), meterRegistry, Tags.empty()); + + jooq.execute("CREATE TABLE author (" + + " id int NOT NULL," + + " first_name varchar(255) DEFAULT NULL," + + " last_name varchar(255) DEFAULT NULL," + + " PRIMARY KEY (id)" + + ")"); + + jooq.execute("INSERT INTO author VALUES(1, 'jon', 'schneider')"); + return jooq; + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsIntegrationTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsIntegrationTest.java new file mode 100644 index 0000000000..8ad78a8859 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsIntegrationTest.java @@ -0,0 +1,185 @@ +/* + * Copyright 2022 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.binder.db; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.db.PostgreSQLDatabaseMetrics; +import io.micrometer.core.instrument.search.RequiredSearch; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BLOCKS_HITS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BLOCKS_READS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_BACKEND; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_CHECKPOINT; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_CLEAN; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CHECKPOINTS_REQUESTED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CHECKPOINTS_TIMED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CONNECTIONS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.LOCKS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_DEAD; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_DELETED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_FETCHED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_INSERTED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_UPDATED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.SIZE; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.TEMP_WRITES; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.TRANSACTIONS; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Markus Dobel + */ +@Testcontainers +@Tag("docker") +class PostgreSQLDatabaseMetricsIntegrationTest { + + @Container + private final PostgreSQLContainer postgres = new PostgreSQLContainer<>(getDockerImageName()); + private final MeterRegistry registry = new SimpleMeterRegistry(); + + private DataSource dataSource; + private Tags tags; + + // statistics are updated only every PGSTAT_STAT_INTERVAL, which is 500ms. Add a bit for stable tests. + private static final long PGSTAT_STAT_INTERVAL = 500L + 50L; + + @BeforeEach + void setup() { + dataSource = createDataSource(); + tags = Tags.of("database", postgres.getDatabaseName()); + + new PostgreSQLDatabaseMetrics(dataSource, postgres.getDatabaseName()).bindTo(registry); + } + + @Test + void gaugesAreNotZero() throws Exception { + /* create noise to increment gauges */ + executeSql( + "CREATE TABLE gauge_test_table (val varchar(255))", + "INSERT INTO gauge_test_table (val) VALUES ('foo')", + "UPDATE gauge_test_table SET val = 'bar'", + "SELECT * FROM gauge_test_table", + "DELETE FROM gauge_test_table" + ); + Thread.sleep(PGSTAT_STAT_INTERVAL); + + final List GAUGES = Arrays.asList( + SIZE, CONNECTIONS, ROWS_DEAD, LOCKS + ); + + for (String name: GAUGES) { + assertThat(get(name).gauge().value()) + .withFailMessage("Gauge " + name + " is zero.") + .isGreaterThan(0); + } + } + + @Test + void countersAreNotZero() throws Exception { + /* create noise to increment counters */ + executeSql( + "CREATE TABLE counter_test_table (val varchar(255))", + "INSERT INTO counter_test_table (val) VALUES ('foo')", + "UPDATE counter_test_table SET val = 'bar'", + "SELECT * FROM counter_test_table", + "DELETE FROM counter_test_table" + ); + Thread.sleep(PGSTAT_STAT_INTERVAL); + + final List COUNTERS = Arrays.asList( + BLOCKS_HITS, BLOCKS_READS, + TRANSACTIONS, + ROWS_FETCHED, ROWS_INSERTED, ROWS_UPDATED, ROWS_DELETED, + BUFFERS_CHECKPOINT + ); + + /* the following counters are zero on a clean database and hard to increase reliably */ + final List ZERO_COUNTERS = Arrays.asList( + TEMP_WRITES, + CHECKPOINTS_TIMED, CHECKPOINTS_REQUESTED, + BUFFERS_CLEAN, BUFFERS_BACKEND + ); + + for (String name: COUNTERS) { + assertThat(get(name).functionCounter().count()) + .withFailMessage("Counter " + name + " is zero.") + .isGreaterThan(0); + } + } + + @Test + void deadTuplesGaugeIncreases() throws Exception { + final double deadRowsBefore = get(ROWS_DEAD).gauge().value(); + + executeSql( + "CREATE TABLE dead_tuples_test_table (val varchar(255))", + "INSERT INTO dead_tuples_test_table (val) VALUES ('foo')", + "UPDATE dead_tuples_test_table SET val = 'bar'" + ); + + // wait for stats to be updated + Thread.sleep(PGSTAT_STAT_INTERVAL); + assertThat(get(ROWS_DEAD).gauge().value()).isGreaterThan(deadRowsBefore); + } + + private DataSource createDataSource() { + final PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setURL(postgres.getJdbcUrl()); + dataSource.setUser(postgres.getUsername()); + dataSource.setPassword(postgres.getPassword()); + dataSource.setDatabaseName(postgres.getDatabaseName()); + return dataSource; + } + + private void executeSql(String... statements) throws SQLException { + try (final Connection connection = dataSource.getConnection()) { + executeSql(connection, statements); + } + } + + private void executeSql(Connection connection, String... statements) throws SQLException { + for (String sql : statements) { + try (final PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.execute(); + } + } + } + + private RequiredSearch get(final String name) { + return registry.get(name).tags(tags); + } + + private static DockerImageName getDockerImageName() { + return DockerImageName.parse("postgres:9.6.24"); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsTest.java new file mode 100644 index 0000000000..816b7d6205 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/db/PostgreSQLDatabaseMetricsTest.java @@ -0,0 +1,121 @@ +/* + * Copyright 2019 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.binder.db; + +import javax.sql.DataSource; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.db.PostgreSQLDatabaseMetrics; +import io.micrometer.core.instrument.search.RequiredSearch; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BLOCKS_HITS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BLOCKS_READS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_BACKEND; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_CHECKPOINT; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.BUFFERS_CLEAN; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CHECKPOINTS_REQUESTED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CHECKPOINTS_TIMED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.CONNECTIONS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.LOCKS; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_DEAD; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_DELETED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_FETCHED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_INSERTED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.ROWS_UPDATED; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.SIZE; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.TEMP_WRITES; +import static io.micrometer.binder.db.PostgreSQLDatabaseMetrics.Names.TRANSACTIONS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * @author Kristof Depypere + * @author Markus Dobel + */ +class PostgreSQLDatabaseMetricsTest { + private static final String DATABASE_NAME = "test"; + private static final String FUNCTIONAL_COUNTER_KEY = "key"; + private final DataSource dataSource = mock(DataSource.class); + private final MeterRegistry registry = new SimpleMeterRegistry(); + private final Tags tags = Tags.of("database", DATABASE_NAME); + + @Test + void shouldRegisterPostgreSqlMetrics() { + io.micrometer.binder.db.PostgreSQLDatabaseMetrics metrics = new io.micrometer.binder.db.PostgreSQLDatabaseMetrics(dataSource, DATABASE_NAME); + metrics.bindTo(registry); + + get(SIZE).gauge(); + get(CONNECTIONS).gauge(); + get(ROWS_FETCHED).functionCounter(); + get(ROWS_INSERTED).functionCounter(); + get(ROWS_UPDATED).functionCounter(); + get(ROWS_DELETED).functionCounter(); + get(ROWS_DEAD).gauge(); + + get(BLOCKS_HITS).functionCounter(); + get(BLOCKS_READS).functionCounter(); + + get(TEMP_WRITES).functionCounter(); + get(LOCKS).gauge(); + + get(TRANSACTIONS).functionCounter(); + get(CHECKPOINTS_TIMED).functionCounter(); + get(CHECKPOINTS_REQUESTED).functionCounter(); + + get(BUFFERS_CHECKPOINT).functionCounter(); + get(BUFFERS_CLEAN).functionCounter(); + get(BUFFERS_BACKEND).functionCounter(); + } + + @Test + void shouldBridgePgStatReset() { + io.micrometer.binder.db.PostgreSQLDatabaseMetrics metrics = new io.micrometer.binder.db.PostgreSQLDatabaseMetrics(dataSource, DATABASE_NAME); + metrics.bindTo(registry); + + metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 5); + metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 10); + + // first reset + Double result = metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 5); + + // then + assertThat(result).isEqualTo(15); + } + + @Test + void shouldBridgeDoublePgStatReset() { + io.micrometer.binder.db.PostgreSQLDatabaseMetrics metrics = new PostgreSQLDatabaseMetrics(dataSource, DATABASE_NAME); + metrics.bindTo(registry); + + metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 5); + metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 10); + + // first reset + metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 3); + + // second reset + Double result = metrics.resettableFunctionalCounter(FUNCTIONAL_COUNTER_KEY, () -> 1); + + assertThat(result).isEqualTo(14); + } + + private RequiredSearch get(final String name) { + return registry.get(name).tags(tags); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptorTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptorTest.java new file mode 100644 index 0000000000..743381bc4f --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpClientInterceptorTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 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.binder.httpcomponents; + +import java.util.concurrent.Future; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.httpcomponents.DefaultUriMapper; +import io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import ru.lanwen.wiremock.ext.WiremockResolver; + +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor}. + * + * @author Jon Schneider + * @author Johnny Lim + */ +@ExtendWith(WiremockResolver.class) +class MicrometerHttpClientInterceptorTest { + private MeterRegistry registry; + + @BeforeEach + void setup() { + registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + } + + @Test + void asyncRequest(@WiremockResolver.Wiremock WireMockServer server) throws Exception { + server.stubFor(any(anyUrl())); + CloseableHttpAsyncClient client = asyncClient(); + client.start(); + HttpGet request = new HttpGet(server.baseUrl()); + + Future future = client.execute(request, null); + HttpResponse response = future.get(); + + assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); + assertThat(registry.get("httpcomponents.httpclient.request").timer().count()).isEqualTo(1); + + client.close(); + } + + @Test + void uriIsReadFromHttpHeader(@WiremockResolver.Wiremock WireMockServer server) throws Exception { + server.stubFor(any(anyUrl())); + io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor interceptor = new io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor(registry, Tags.empty(), true); + CloseableHttpAsyncClient client = asyncClient(interceptor); + client.start(); + HttpGet request = new HttpGet(server.baseUrl()); + request.addHeader(DefaultUriMapper.URI_PATTERN_HEADER, "/some/pattern"); + + Future future = client.execute(request, null); + HttpResponse response = future.get(); + + assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); + assertThat(registry.get("httpcomponents.httpclient.request").tag("uri", "/some/pattern").tag("status", "200").timer().count()) + .isEqualTo(1); + + client.close(); + } + + private CloseableHttpAsyncClient asyncClient() { + io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor interceptor = new io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor(registry, + request -> request.getRequestLine().getUri(), + Tags.empty(), + true); + return asyncClient(interceptor); + } + + private CloseableHttpAsyncClient asyncClient(MicrometerHttpClientInterceptor interceptor) { + return HttpAsyncClients.custom() + .addInterceptorFirst(interceptor.getRequestInterceptor()) + .addInterceptorLast(interceptor.getResponseInterceptor()) + .build(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java new file mode 100644 index 0000000000..c4e8ba8e2a --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/MicrometerHttpRequestExecutorTest.java @@ -0,0 +1,247 @@ +/* + * Copyright 2017 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.binder.httpcomponents; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.httpcomponents.DefaultUriMapper; +import io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor; +import io.micrometer.core.instrument.search.MeterNotFoundException; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.protocol.HttpRequestExecutor; +import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import ru.lanwen.wiremock.ext.WiremockResolver; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests for {@link io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor}. + * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + */ +@ExtendWith(WiremockResolver.class) +class MicrometerHttpRequestExecutorTest { + + private static final String EXPECTED_METER_NAME = "httpcomponents.httpclient.request"; + + private MeterRegistry registry; + + @BeforeEach + void setup() { + registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + } + + @Test + void timeSuccessful(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(false)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .timer().count()).isEqualTo(1L); + } + + @Test + void httpMethodIsTagged(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(false)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + EntityUtils.consume(client.execute(new HttpPost(server.baseUrl())).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("method", "GET") + .timer().count()).isEqualTo(2L); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("method", "POST") + .timer().count()).isEqualTo(1L); + } + + @Test + void httpStatusCodeIsTagged(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(urlEqualTo("/ok")).willReturn(aResponse().withStatus(200))); + server.stubFor(any(urlEqualTo("/notfound")).willReturn(aResponse().withStatus(404))); + server.stubFor(any(urlEqualTo("/error")).willReturn(aResponse().withStatus(500))); + HttpClient client = client(executor(false)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/ok")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/ok")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/notfound")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/error")).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("method", "GET", "status", "200") + .timer().count()).isEqualTo(2L); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("method", "GET", "status", "404") + .timer().count()).isEqualTo(1L); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("method", "GET", "status", "500") + .timer().count()).isEqualTo(1L); + } + + @Test + void uriIsUnknownByDefault(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(false)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/someuri")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/otheruri")).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("uri", "UNKNOWN") + .timer().count()).isEqualTo(3L); + } + + @Test + void uriIsReadFromHttpHeader(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(false)); + HttpGet getWithHeader = new HttpGet(server.baseUrl()); + getWithHeader.addHeader(DefaultUriMapper.URI_PATTERN_HEADER, "/some/pattern"); + EntityUtils.consume(client.execute(getWithHeader).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("uri", "/some/pattern") + .timer().count()).isEqualTo(1L); + assertThrows(MeterNotFoundException.class, () -> registry.get(EXPECTED_METER_NAME) + .tags("uri", "UNKNOWN") + .timer()); + } + + @Test + void routeNotTaggedByDefault(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(false)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + List tagKeys = registry.get(EXPECTED_METER_NAME) + .timer().getId().getTags().stream() + .map(Tag::getKey) + .collect(Collectors.toList()); + assertThat(tagKeys).doesNotContain("target.scheme", "target.host", "target.port"); + assertThat(tagKeys).contains("status", "method"); + } + + @Test + void routeTaggedIfEnabled(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + HttpClient client = client(executor(true)); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + List tagKeys = registry.get(EXPECTED_METER_NAME) + .timer().getId().getTags().stream() + .map(Tag::getKey) + .collect(Collectors.toList()); + assertThat(tagKeys).contains("target.scheme", "target.host", "target.port"); + } + + @Test + void waitForContinueGetsPassedToSuper() { + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor requestExecutor = io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(registry) + .waitForContinue(1000) + .build(); + assertThat(requestExecutor).hasFieldOrPropertyWithValue("waitForContinue", 1000); + } + + @Test + void uriMapperWorksAsExpected(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor executor = io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(registry) + .uriMapper(request -> request.getRequestLine().getUri()) + .build(); + HttpClient client = client(executor); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/foo")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/bar")).getEntity()); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl() + "/foo")).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("uri", "/") + .timer().count()).isEqualTo(1L); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("uri", "/foo") + .timer().count()).isEqualTo(2L); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("uri", "/bar") + .timer().count()).isEqualTo(1L); + } + + @Test + void additionalTagsAreExposed(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor executor = io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(registry) + .tags(Tags.of("foo", "bar", "some.key", "value")) + .exportTagsForRoute(true) + .build(); + HttpClient client = client(executor); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME) + .tags("foo", "bar", "some.key", "value", "target.host", "localhost") + .timer().count()).isEqualTo(1L); + } + + @Test + void settingNullRegistryThrowsException() { + assertThrows(IllegalArgumentException.class, () -> + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(null) + .build()); + } + + @Test + void overridingUriMapperWithNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(registry) + .uriMapper(null) + .build() + ); + } + + @Test + void overrideExtraTagsDoesNotThrowAnException(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor executor = io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor.builder(registry) + .tags(null) + .build(); + HttpClient client = client(executor); + EntityUtils.consume(client.execute(new HttpGet(server.baseUrl())).getEntity()); + assertThat(registry.get(EXPECTED_METER_NAME)).isNotNull(); + } + + private HttpClient client(HttpRequestExecutor executor) { + return HttpClientBuilder.create() + .setRequestExecutor(executor) + .build(); + } + + private HttpRequestExecutor executor(boolean exportRoutes) { + return MicrometerHttpRequestExecutor.builder(registry) + .exportTagsForRoute(exportRoutes) + .build(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java new file mode 100644 index 0000000000..35d39c5f9d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinderTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2017 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.binder.httpcomponents; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.binder.httpcomponents.PoolingHttpClientConnectionManagerMetricsBinder; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.pool.ConnPoolControl; +import org.apache.http.pool.PoolStats; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link io.micrometer.binder.httpcomponents.PoolingHttpClientConnectionManagerMetricsBinder}. + * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + */ +class PoolingHttpClientConnectionManagerMetricsBinderTest { + + private MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private ConnPoolControl connPoolControl; + private io.micrometer.binder.httpcomponents.PoolingHttpClientConnectionManagerMetricsBinder binder; + + @BeforeEach + @SuppressWarnings("unchecked") + void setup() { + connPoolControl = (ConnPoolControl) mock(ConnPoolControl.class); + binder = new PoolingHttpClientConnectionManagerMetricsBinder(connPoolControl, "test"); + binder.bindTo(registry); + } + + @Test + void totalMax() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getMax()).thenReturn(13); + when(connPoolControl.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.max") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(13.0); + } + + @Test + void totalAvailable() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getAvailable()).thenReturn(17); + when(connPoolControl.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.connections") + .tags("httpclient", "test", "state", "available") + .gauge().value()).isEqualTo(17.0); + } + + @Test + void totalLeased() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getLeased()).thenReturn(23); + when(connPoolControl.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.connections") + .tags("httpclient", "test", "state", "leased") + .gauge().value()).isEqualTo(23.0); + } + + @Test + void totalPending() { + PoolStats poolStats = mock(PoolStats.class); + when(poolStats.getPending()).thenReturn(37); + when(connPoolControl.getTotalStats()).thenReturn(poolStats); + assertThat(registry.get("httpcomponents.httpclient.pool.total.pending") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(37.0); + } + + @Test + void routeMaxDefault() { + when(connPoolControl.getDefaultMaxPerRoute()).thenReturn(7); + assertThat(registry.get("httpcomponents.httpclient.pool.route.max.default") + .tags("httpclient", "test") + .gauge().value()).isEqualTo(7.0); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommandTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommandTest.java new file mode 100644 index 0000000000..87f16fbbca --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommandTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import com.netflix.hystrix.Hystrix; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.hystrix.HystrixEventType; +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.hystrix.MicrometerMetricsPublisher; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MicrometerMetricsPublisherCommandTest { + private static HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("MicrometerGROUP"); + private HystrixCommandProperties.Setter propertiesSetter; + private MeterRegistry registry = new SimpleMeterRegistry(); + + @BeforeEach + void init() { + Hystrix.reset(); + propertiesSetter = HystrixCommandProperties.Setter() + .withCircuitBreakerEnabled(true) + .withCircuitBreakerRequestVolumeThreshold(20) + .withCircuitBreakerSleepWindowInMilliseconds(10_000) + .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) + .withExecutionTimeoutInMilliseconds(100); + } + + @AfterEach + void teardown() { + Hystrix.reset(); + } + + @Test + void cumulativeCounters() throws Exception { + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerMetricsPublisher(new io.micrometer.binder.hystrix.MicrometerMetricsPublisher(registry, metricsPublisher)); + HystrixCommandKey key = HystrixCommandKey.Factory.asKey("MicrometerCOMMAND-A"); + + for (int i = 0; i < 3; i++) { + new SuccessCommand(key).execute(); + new SuccessCommand(key).execute(); + new SuccessCommand(key).execute(); + Thread.sleep(10); + new TimeoutCommand(key).execute(); + new SuccessCommand(key).execute(); + new FailureCommand(key).execute(); + new FailureCommand(key).execute(); + new SuccessCommand(key).execute(); + new SuccessCommand(key).execute(); + new SuccessCommand(key).execute(); + Thread.sleep(10); + new SuccessCommand(key).execute(); + } + + Iterable tags = Tags.of("group", "MicrometerGROUP", "key", "MicrometerCOMMAND-A"); + + assertExecutionMetric(tags, HystrixEventType.SUCCESS, 24.0); + assertExecutionMetric(tags, HystrixEventType.TIMEOUT, 3.0); + assertExecutionMetric(tags, HystrixEventType.FAILURE, 6.0); + assertExecutionMetric(tags, HystrixEventType.SHORT_CIRCUITED, 0.0); + assertThat(registry.get("hystrix.circuit.breaker.open").tags(tags).gauge().value()).isEqualTo(0.0); + } + + private void assertExecutionMetric(Iterable tags, HystrixEventType eventType, double count) { + Iterable myTags = Tags.concat(tags, "event", eventType.name().toLowerCase(), + "terminal", Boolean.toString(eventType.isTerminal())); + assertThat(registry.get("hystrix.execution").tags(myTags) + .counter() + .count()).isEqualTo(count); + } + + @Test + void openCircuit() { + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerMetricsPublisher(new MicrometerMetricsPublisher(registry, metricsPublisher)); + HystrixCommandKey key = HystrixCommandKey.Factory.asKey("MicrometerCOMMAND-B"); + + propertiesSetter.withCircuitBreakerForceOpen(true); + + new SuccessCommand(key).execute(); + new SuccessCommand(key).execute(); + new TimeoutCommand(key).execute(); + new FailureCommand(key).execute(); + new FailureCommand(key).execute(); + new SuccessCommand(key).execute(); + + Iterable tags = Tags.of("group", groupKey.name(), "key", key.name()); + + assertExecutionMetric(tags, HystrixEventType.SHORT_CIRCUITED, 6.0); + assertExecutionMetric(tags, HystrixEventType.SUCCESS, 0.0); + assertExecutionMetric(tags, HystrixEventType.TIMEOUT, 0.0); + assertExecutionMetric(tags, HystrixEventType.FAILURE, 0.0); + assertThat(registry.get("hystrix.circuit.breaker.open").tags(tags).gauge().value()).isEqualTo(1.0); + } + + class SampleCommand extends HystrixCommand { + boolean shouldFail; + int latencyToAdd; + + SampleCommand(HystrixCommandKey key, boolean shouldFail, int latencyToAdd) { + super(Setter.withGroupKey(groupKey).andCommandKey(key).andCommandPropertiesDefaults(propertiesSetter)); + this.shouldFail = shouldFail; + this.latencyToAdd = latencyToAdd; + } + + @Override + protected Integer run() throws Exception { + if (shouldFail) { + throw new RuntimeException("command failure"); + } else { + Thread.sleep(latencyToAdd); + return 1; + } + } + + @Override + protected Integer getFallback() { + return 99; + } + } + + class SuccessCommand extends SampleCommand { + SuccessCommand(HystrixCommandKey key) { + super(key, false, 0); + } + + public SuccessCommand(HystrixCommandKey key, int latencyToAdd) { + super(key, false, latencyToAdd); + } + } + + class FailureCommand extends SampleCommand { + FailureCommand(HystrixCommandKey key) { + super(key, true, 0); + } + + public FailureCommand(HystrixCommandKey key, int latencyToAdd) { + super(key, true, latencyToAdd); + } + } + + class TimeoutCommand extends SampleCommand { + TimeoutCommand(HystrixCommandKey key) { + super(key, false, 400); //exceeds 100ms timeout + } + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPoolTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPoolTest.java new file mode 100644 index 0000000000..2c78283e16 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPoolTest.java @@ -0,0 +1,142 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import com.netflix.hystrix.Hystrix; +import com.netflix.hystrix.HystrixCommand; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import io.micrometer.core.instrument.Meter.Type; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.hystrix.MicrometerMetricsPublisher; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MicrometerMetricsPublisherThreadPoolTest { + private static final String NAME_HYSTRIX_THREADPOOL = "hystrix.threadpool"; + private static final String NAME_MICROMETER_GROUP = "MicrometerGROUP"; + + private MeterRegistry registry = new SimpleMeterRegistry(); + private HystrixCommandProperties.Setter propertiesSetter = HystrixCommandProperties.Setter() + .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD); + private final HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(NAME_MICROMETER_GROUP); + + @BeforeEach + void init() { + Hystrix.reset(); + } + + @AfterEach + void teardown() { + Hystrix.reset(); + } + + /** + * Test that thread pool metrics are published. + */ + @Test + void testMetricIds() { + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + HystrixPlugins.reset(); + HystrixPlugins.getInstance().registerMetricsPublisher(new MicrometerMetricsPublisher(registry, metricsPublisher)); + + HystrixCommandKey key = HystrixCommandKey.Factory.asKey("MicrometerCOMMAND-A"); + new SampleCommand(key).execute(); + + final Tags tags = Tags.of("key", NAME_MICROMETER_GROUP); + + final Set actualMeterIds = registry.getMeters().stream() + .map(meter -> new MeterId(meter.getId().getName(), meter.getId().getType(), Tags.of(meter.getId().getTags()))) + .collect(Collectors.toSet()); + + final Set expectedMeterIds = new HashSet<>(Arrays.asList( + new MeterId(metricName("threads.active.current.count"), Type.GAUGE, tags), + new MeterId(metricName("threads.cumulative.count"), Type.COUNTER, tags.and(Tags.of("type", "executed"))), + new MeterId(metricName("threads.cumulative.count"), Type.COUNTER, tags.and(Tags.of("type", "rejected"))), + new MeterId(metricName("threads.pool.current.size"), Type.GAUGE, tags), + new MeterId(metricName("threads.largest.pool.current.size"), Type.GAUGE, tags), + new MeterId(metricName("threads.max.pool.current.size"), Type.GAUGE, tags), + new MeterId(metricName("threads.core.pool.current.size"), Type.GAUGE, tags), + new MeterId(metricName("tasks.cumulative.count"), Type.COUNTER, tags.and(Tags.of("type", "scheduled"))), + new MeterId(metricName("tasks.cumulative.count"), Type.COUNTER, tags.and(Tags.of("type", "completed"))), + new MeterId(metricName("queue.current.size"), Type.GAUGE, tags), + new MeterId(metricName("queue.max.size"), Type.GAUGE, tags), + new MeterId(metricName("queue.rejection.threshold.size"), Type.GAUGE, tags) + )); + + assertThat(actualMeterIds).containsAll(expectedMeterIds); + } + + private class SampleCommand extends HystrixCommand { + SampleCommand(HystrixCommandKey key) { + super(Setter.withGroupKey(groupKey).andCommandKey(key).andCommandPropertiesDefaults(propertiesSetter)); + } + + @Override + protected Integer run() { + return 1; + } + } + + private static class MeterId { + private final String name; + private final Type type; + private final Tags tags; + + MeterId(final String name, final Type type, final Tags tags) { + this.name = name; + this.type = type; + this.tags = tags; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final MeterId meterId = (MeterId) o; + return Objects.equals(name, meterId.name) + && type == meterId.type + && Objects.equals(tags, meterId.tags); + } + + @Override + public int hashCode() { + return Objects.hash(name, type, tags); + } + } + + private static String metricName(String name) { + return String.join(".", NAME_HYSTRIX_THREADPOOL, name); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProviderTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProviderTest.java new file mode 100644 index 0000000000..a415612b32 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProviderTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.NotAcceptableException; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.internal.monitoring.RequestEventImpl.Builder; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent.Type; +import org.glassfish.jersey.uri.UriTemplate; +import org.junit.jupiter.api.Test; + +import static java.util.stream.StreamSupport.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.jersey.server.DefaultJerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class DefaultJerseyTagsProviderTest { + + private final io.micrometer.binder.jersey.server.DefaultJerseyTagsProvider tagsProvider = new DefaultJerseyTagsProvider(); + + @Test + void testRootPath() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("root", 200, null, "SUCCESS")); + } + + @Test + void templatedPathsAreReturned() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/", "/", "/hello/{name}"))) + .containsExactlyInAnyOrder(tagsFrom("/hello/{name}", 200, null, "SUCCESS")); + } + + @Test + void applicationPathIsPresent() { + assertThat(tagsProvider.httpRequestTags(event(200, null, "/app", "/", "/hello"))) + .containsExactlyInAnyOrder(tagsFrom("/app/hello", 200, null, "SUCCESS")); + } + + @Test + void notFoundsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(404, null, "/app", "/", "/not-found"))) + .containsExactlyInAnyOrder(tagsFrom("NOT_FOUND", 404, null, "CLIENT_ERROR")); + } + + @Test + void redirectsAreShunted() { + assertThat(tagsProvider.httpRequestTags(event(301, null, "/app", "/", "/redirect301"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 301, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(302, null, "/app", "/", "/redirect302"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 302, null, "REDIRECTION")); + assertThat(tagsProvider.httpRequestTags(event(399, null, "/app", "/", "/redirect399"))) + .containsExactlyInAnyOrder(tagsFrom("REDIRECTION", 399, null, "REDIRECTION")); + } + + @Test + @SuppressWarnings("serial") + void exceptionsAreMappedCorrectly() { + assertThat(tagsProvider.httpRequestTags( + event(500, new IllegalArgumentException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "IllegalArgumentException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags(event(500, + new IllegalArgumentException(new NullPointerException()), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "NullPointerException", "SERVER_ERROR")); + assertThat(tagsProvider.httpRequestTags( + event(406, new NotAcceptableException(), "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 406, "NotAcceptableException", "CLIENT_ERROR")); + assertThat(tagsProvider.httpRequestTags( + event(500, new Exception("anonymous") { }, "/app", (String[]) null))) + .containsExactlyInAnyOrder(tagsFrom("/app", 500, "io.micrometer.binder.jersey.server.DefaultJerseyTagsProviderTest$1", "SERVER_ERROR")); + } + + @Test + void longRequestTags() { + assertThat(tagsProvider.httpLongRequestTags(event(0, null, "/app", (String[]) null))) + .containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/app")); + } + + private static RequestEvent event(Integer status, Exception exception, String baseUri, String... uriTemplateStrings) { + Builder builder = new Builder(); + + ContainerRequest containerRequest = mock(ContainerRequest.class); + when(containerRequest.getMethod()).thenReturn("GET"); + builder.setContainerRequest(containerRequest); + + ContainerResponse containerResponse = mock(ContainerResponse.class); + when(containerResponse.getStatus()).thenReturn(status); + builder.setContainerResponse(containerResponse); + + builder.setException(exception, null); + + ExtendedUriInfo extendedUriInfo = mock(ExtendedUriInfo.class); + when(extendedUriInfo.getBaseUri()).thenReturn( + URI.create("http://localhost:8080" + (baseUri == null ? "/" : baseUri))); + List uriTemplates = uriTemplateStrings == null ? Collections.emptyList() + : Arrays.stream(uriTemplateStrings).map(uri -> new UriTemplate(uri)) + .collect(Collectors.toList()); + // UriTemplate are returned in reverse order + Collections.reverse(uriTemplates); + when(extendedUriInfo.getMatchedTemplates()).thenReturn(uriTemplates); + builder.setExtendedUriInfo(extendedUriInfo); + + return builder.build(Type.FINISHED); + } + + private static Tag[] tagsFrom(String uri, int status, String exception, String outcome) { + Iterable expectedTags = Tags.of( + "method", "GET", + "uri", uri, + "status", String.valueOf(status), + "exception", exception == null ? "None" : exception, + "outcome", outcome + ); + + return stream(expectedTags.spliterator(), false) + .toArray(Tag[]::new); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTest.java new file mode 100644 index 0000000000..77c38e5cf6 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Application; + +import io.micrometer.binder.jersey.server.mapper.ResourceGoneExceptionMapper; +import io.micrometer.binder.jersey.server.resources.TestResource; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.jersey.server.MetricsApplicationEventListener}. + * + * @author Michael Weirauch + * @author Johnny Lim + */ +class MetricsRequestEventListenerTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + + final io.micrometer.binder.jersey.server.MetricsApplicationEventListener listener = new MetricsApplicationEventListener( + registry, new DefaultJerseyTagsProvider(), METRIC_NAME, true); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register(TestResource.class); + config.register(ResourceGoneExceptionMapper.class); + + return config; + } + + @Test + void resourcesAreTimed() { + target("/").request().get(); + target("hello").request().get(); + target("hello/").request().get(); + target("hello/peter").request().get(); + target("sub-resource/sub-hello/peter").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("root", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/hello", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(2); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/hello/{name}", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/sub-resource/sub-hello/{name}", "200", "SUCCESS", null)).timer().count()) + .isEqualTo(1); + + // assert we are not auto-timing long task @Timed + assertThat(registry.getMeters()).hasSize(4); + } + + @Test + void notFoundIsAccumulatedUnderSameUri() { + try { + target("not-found").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("NOT_FOUND", "404", "CLIENT_ERROR", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void notFoundIsReportedWithUriOfMatchedResource() { + try { + target("throws-not-found-exception").request().get(); + } + catch (NotFoundException ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-not-found-exception", "404", "CLIENT_ERROR", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void redirectsAreAccumulatedUnderSameUri() { + target("redirect/302").request().get(); + target("redirect/307").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("REDIRECTION", "302", "REDIRECTION", null)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("REDIRECTION", "307", "REDIRECTION", null)).timer().count()) + .isEqualTo(1); + } + + @Test + void exceptionsAreMappedCorrectly() { + try { + target("throws-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-webapplication-exception").request().get(); + } + catch (Exception ignored) { + } + try { + target("throws-mappable-exception").request().get(); + } + catch (Exception ignored) { + } + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/throws-exception", "500", "SERVER_ERROR", "IllegalArgumentException")) + .timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME).tags( + tagsFrom("/throws-webapplication-exception", "401", "CLIENT_ERROR", "NotAuthorizedException")) + .timer().count()) + .isEqualTo(1); + + assertThat(registry.get(METRIC_NAME).tags( + tagsFrom("/throws-mappable-exception", "410", "CLIENT_ERROR", "ResourceGoneException")) + .timer().count()) + .isEqualTo(1); + } + + private static Iterable tagsFrom(String uri, String status, String outcome, String exception) { + return Tags.of("method", "GET", "uri", uri, "status", status, "outcome", outcome, + "exception", exception == null ? "None" : exception); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTimedTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTimedTest.java new file mode 100644 index 0000000000..a099317e43 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/MetricsRequestEventListenerTimedTest.java @@ -0,0 +1,204 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Response; + +import io.micrometer.binder.Issue; +import io.micrometer.binder.jersey.server.resources.TimedOnClassResource; +import io.micrometer.binder.jersey.server.resources.TimedResource; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Michael Weirauch + */ +class MetricsRequestEventListenerTimedTest extends JerseyTest { + + static { + Logger.getLogger("org.glassfish.jersey").setLevel(Level.OFF); + } + + private static final String METRIC_NAME = "http.server.requests"; + + private MeterRegistry registry; + + private CountDownLatch longTaskRequestStartedLatch; + + private CountDownLatch longTaskRequestReleaseLatch; + + @Override + protected Application configure() { + registry = new SimpleMeterRegistry(); + longTaskRequestStartedLatch = new CountDownLatch(1); + longTaskRequestReleaseLatch = new CountDownLatch(1); + + final io.micrometer.binder.jersey.server.MetricsApplicationEventListener listener = new MetricsApplicationEventListener( + registry, new DefaultJerseyTagsProvider(), METRIC_NAME, false); + + final ResourceConfig config = new ResourceConfig(); + config.register(listener); + config.register( + new TimedResource(longTaskRequestStartedLatch, longTaskRequestReleaseLatch)); + config.register(TimedOnClassResource.class); + + return config; + } + + @Test + void resourcesAndNotFoundsAreNotAutoTimed() { + target("not-timed").request().get(); + target("not-found").request().get(); + + assertThat(registry.find(METRIC_NAME) + .tags(tagsFrom("/not-timed", 200)).timer()).isNull(); + + assertThat(registry.find(METRIC_NAME) + .tags(tagsFrom("NOT_FOUND", 404)).timer()).isNull(); + } + + @Test + void resourcesWithAnnotationAreTimed() { + target("timed").request().get(); + target("multi-timed").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/timed", 200)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get("multi1") + .tags(tagsFrom("/multi-timed", 200)).timer().count()) + .isEqualTo(1); + + assertThat(registry.get("multi2") + .tags(tagsFrom("/multi-timed", 200)).timer().count()) + .isEqualTo(1); + } + + @Test + void longTaskTimerSupported() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client + * processing might be slower in triggering the request resulting in the + * assertions below to fail. Thread.sleep() is not an option, so resort + * to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the request is not timed, yet + assertThat(registry.find(METRIC_NAME).tags(tagsFrom("/timed", 200)).timer()) + .isNull(); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/long-timed")) + .longTaskTimer().activeTasks()) + .isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // the request is timed after the long running request completed + assertThat(registry.get(METRIC_NAME) + .tags(tagsFrom("/long-timed", 200)) + .timer().count()) + .isEqualTo(1); + } + + @Test + @Issue("gh-2861") + void longTaskTimerOnlyOneMeter() throws InterruptedException, ExecutionException, TimeoutException { + final Future future = target("just-long-timed").request().async().get(); + + /* + * Wait until the request has arrived at the server side. (Async client + * processing might be slower in triggering the request resulting in the + * assertions below to fail. Thread.sleep() is not an option, so resort + * to CountDownLatch.) + */ + longTaskRequestStartedLatch.await(5, TimeUnit.SECONDS); + + // the long running task is timed + assertThat(registry.get("long.task.in.request") + .tags(Tags.of("method", "GET", "uri", "/just-long-timed")) + .longTaskTimer().activeTasks()) + .isEqualTo(1); + + // finish the long running request + longTaskRequestReleaseLatch.countDown(); + future.get(5, TimeUnit.SECONDS); + + // no meters registered except the one checked above + assertThat(registry.getMeters().size()).isOne(); + } + + @Test + void unnamedLongTaskTimerIsNotSupported() { + assertThatExceptionOfType(ProcessingException.class) + .isThrownBy(() -> target("long-timed-unnamed").request().get()) + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void classLevelAnnotationIsInherited() { + target("/class/inherited").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/inherited", 200), + Tags.of("on", "class"))) + .timer().count()) + .isEqualTo(1); + } + + @Test + void methodLevelAnnotationOverridesClassLevel() { + target("/class/on-method").request().get(); + + assertThat(registry.get(METRIC_NAME) + .tags(Tags.concat(tagsFrom("/class/on-method", 200), + Tags.of("on", "method"))) + .timer().count()) + .isEqualTo(1); + + // class level annotation is not picked up + assertThat(registry.getMeters()).hasSize(1); + } + + private static Iterable tagsFrom(String uri, int status) { + return Tags.of("method", "GET", "uri", uri, "status", String.valueOf(status), "exception", "None"); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/exception/ResourceGoneException.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/exception/ResourceGoneException.java new file mode 100644 index 0000000000..9b900d8572 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/exception/ResourceGoneException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.binder.jersey.server.exception; + +public class ResourceGoneException extends RuntimeException { + + public ResourceGoneException() { + super(); + } + + public ResourceGoneException(String message) { + super(message); + } + + public ResourceGoneException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/mapper/ResourceGoneExceptionMapper.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/mapper/ResourceGoneExceptionMapper.java new file mode 100644 index 0000000000..1c87c8e929 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/mapper/ResourceGoneExceptionMapper.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 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.binder.jersey.server.mapper; + + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; + +import io.micrometer.binder.jersey.server.exception.ResourceGoneException; + +public class ResourceGoneExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ResourceGoneException exception) { + return Response.status(Status.GONE).build(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TestResource.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TestResource.java new file mode 100644 index 0000000000..5644310d29 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TestResource.java @@ -0,0 +1,106 @@ +/* + * Copyright 2017 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.binder.jersey.server.resources; + +import java.net.URI; + +import javax.ws.rs.GET; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.RedirectionException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import io.micrometer.binder.jersey.server.exception.ResourceGoneException; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TestResource { + + @Produces(MediaType.TEXT_PLAIN) + public static class SubResource { + + @GET + @Path("sub-hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + } + + @GET + public String index() { + return "index"; + } + + @GET + @Path("hello") + public String hello() { + return "hello"; + } + + @GET + @Path("hello/{name}") + public String hello(@PathParam("name") String name) { + return "hello " + name; + } + + @GET + @Path("throws-not-found-exception") + public String throwsNotFoundException() { + throw new NotFoundException(); + } + + @GET + @Path("throws-exception") + public String throwsException() { + throw new IllegalArgumentException(); + } + + @GET + @Path("throws-webapplication-exception") + public String throwsWebApplicationException() { + throw new NotAuthorizedException("notauth", Response.status(Status.UNAUTHORIZED).build()); + } + + @GET + @Path("throws-mappable-exception") + public String throwsMappableException() { + throw new ResourceGoneException("Resource has been permanently removed."); + } + + @GET + @Path("redirect/{status}") + public Response redirect(@PathParam("status") int status) { + if (status == 307) { + throw new RedirectionException(status, URI.create("hello")); + } + return Response.status(status).header("Location", "/hello").build(); + } + + @Path("/sub-resource") + public SubResource subResource() { + return new SubResource(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedOnClassResource.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedOnClassResource.java new file mode 100644 index 0000000000..9af84757df --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedOnClassResource.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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.binder.jersey.server.resources; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +/** + * @author Michael Weirauch + */ +@Path("/class") +@Produces(MediaType.TEXT_PLAIN) +@Timed(extraTags = {"on", "class"}) +public class TimedOnClassResource { + + @GET + @Path("inherited") + public String inherited() { + return "inherited"; + } + + @GET + @Path("on-method") + @Timed(extraTags = {"on", "method"}) + public String onMethod() { + return "on-method"; + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedResource.java b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedResource.java new file mode 100644 index 0000000000..343931e38d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jersey/server/resources/TimedResource.java @@ -0,0 +1,107 @@ +/* + * Copyright 2017 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.binder.jersey.server.resources; + +import java.util.concurrent.CountDownLatch; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import io.micrometer.core.annotation.Timed; + +import static java.util.Objects.requireNonNull; + +/** + * @author Michael Weirauch + */ +@Path("/") +@Produces(MediaType.TEXT_PLAIN) +public class TimedResource { + + private final CountDownLatch longTaskRequestStartedLatch; + + private final CountDownLatch longTaskRequestReleaseLatch; + + public TimedResource(CountDownLatch longTaskRequestStartedLatch, + CountDownLatch longTaskRequestReleaseLatch) { + this.longTaskRequestStartedLatch = requireNonNull(longTaskRequestStartedLatch); + this.longTaskRequestReleaseLatch = requireNonNull(longTaskRequestReleaseLatch); + } + + @GET + @Path("not-timed") + public String notTimed() { + return "not-timed"; + } + + @GET + @Path("timed") + @Timed + public String timed() { + return "timed"; + } + + @GET + @Path("multi-timed") + @Timed("multi1") + @Timed("multi2") + public String multiTimed() { + return "multi-timed"; + } + + /* + * Async server side processing (AsyncResponse) is not supported in the + * in-memory test container. + */ + @GET + @Path("long-timed") + @Timed + @Timed(value = "long.task.in.request", longTask = true) + public String longTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("just-long-timed") + @Timed(value = "long.task.in.request", longTask = true) + public String justLongTimed() { + longTaskRequestStartedLatch.countDown(); + try { + longTaskRequestReleaseLatch.await(); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + return "long-timed"; + } + + @GET + @Path("long-timed-unnamed") + @Timed + @Timed(longTask = true) + public String longTimedUnnamed() { + return "long-timed-unnamed"; + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyClientMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyClientMetricsTest.java new file mode 100644 index 0000000000..082976106a --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyClientMetricsTest.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020 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.binder.jetty; + +import java.util.concurrent.CountDownLatch; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.binder.jetty.JettyClientMetrics; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class JettyClientMetricsTest { + private SimpleMeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private Server server = new Server(0); + private ServerConnector connector = new ServerConnector(server); + + private CountDownLatch singleRequestLatch = new CountDownLatch(1); + private HttpClient httpClient = new HttpClient(); + + @BeforeEach + void beforeEach() throws Exception { + server.insertHandler(new HandlerWrapper() { + @Override + public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) { + switch (request.getPathInfo()) { + case "/errorUnchecked": + throw new RuntimeException("big boom"); + case "/error": + response.setStatus(500); + case "/ok": + baseRequest.setHandled(true); + } + } + }); + server.setConnectors(new Connector[]{connector}); + server.start(); + + httpClient.setFollowRedirects(false); + httpClient.getRequestListeners().add(JettyClientMetrics + .builder(registry, result -> result.getRequest().getURI().getPath()).build()); + + httpClient.addLifeCycleListener(new LifeCycle.Listener() { + @Override + public void lifeCycleStopped(LifeCycle event) { + singleRequestLatch.countDown(); + } + }); + + httpClient.start(); + + } + + @AfterEach + void teardown() throws Exception { + if (server.isRunning()) { + server.stop(); + } + } + + @Test + void successfulHttpPostRequest() throws Exception { + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort() + "/ok"); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.requests") + .tag("outcome", "SUCCESS") + .tag("status", "200") + .tag("uri", "/ok") + .tag("host", "localhost") + .timer().count()).isEqualTo(1); + } + + @Test + void successfulHttpGetRequest() throws Exception { + httpClient.GET("http://localhost:" + connector.getLocalPort() + "/ok"); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.requests") + .tag("outcome", "SUCCESS") + .tag("status", "200") + .tag("uri", "/ok") + .timer().count()).isEqualTo(1); + DistributionSummary requestSizeSummary = registry.get("jetty.client.request.size").summary(); + assertThat(requestSizeSummary.count()).isEqualTo(1); + assertThat(requestSizeSummary.totalAmount()).isEqualTo(0); + } + + @Test + void requestSize() throws Exception { + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort() + "/ok"); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.request.size") + .tag("outcome", "SUCCESS") + .tag("status", "200") + .tag("uri", "/ok") + .tag("host", "localhost") + .summary().totalAmount()).isEqualTo("123456".length()); + } + + @Test + void serverError() throws Exception { + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort() + "/error"); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.requests") + .tag("outcome", "SERVER_ERROR") + .tag("status", "500") + .tag("uri", "/error") + .tag("host", "localhost") + .timer().count()).isEqualTo(1); + } + + @Test + void handlerWrapperUncheckedException() throws Exception { + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort() + "/errorUnchecked"); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.requests") + .tag("outcome", "SERVER_ERROR") + .tag("status", "500") + .tag("uri", "/errorUnchecked") + .tag("host", "localhost") + .timer().count()).isEqualTo(1); + } + + @Test + void notFound() throws Exception { + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort() + "/doesNotExist"); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(singleRequestLatch.await(10, SECONDS)); + assertThat(registry.get("jetty.client.requests") + .tag("outcome", "CLIENT_ERROR") + .tag("status", "404") + .tag("uri", "NOT_FOUND") + .tag("host", "localhost") + .timer().count()).isEqualTo(1); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyConnectionMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyConnectionMetricsTest.java new file mode 100644 index 0000000000..f65f72d01b --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyConnectionMetricsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.util.concurrent.CountDownLatch; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.binder.jetty.JettyConnectionMetrics; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class JettyConnectionMetricsTest { + private SimpleMeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private Server server = new Server(0); + private ServerConnector connector = new ServerConnector(server); + private CloseableHttpClient client = HttpClients.createDefault(); + + void setup() throws Exception { + connector.addBean(new io.micrometer.binder.jetty.JettyConnectionMetrics(registry)); + server.setConnectors(new Connector[]{connector}); + server.start(); + } + + @AfterEach + void teardown() throws Exception { + if (server.isRunning()) { + server.stop(); + } + } + + @Test + void contributesServerConnectorMetrics() throws Exception { + setup(); + HttpPost post = new HttpPost("http://localhost:" + connector.getLocalPort()); + post.setEntity(new StringEntity("123456")); + + try (CloseableHttpResponse ignored = client.execute(post)) { + try (CloseableHttpResponse ignored2 = client.execute(post)) { + assertThat(registry.get("jetty.connections.current").gauge().value()).isEqualTo(2.0); + assertThat(registry.get("jetty.connections.max").gauge().value()).isEqualTo(2.0); + } + } + + CountDownLatch latch = new CountDownLatch(1); + connector.addLifeCycleListener(new LifeCycle.Listener() { + @Override + public void lifeCycleStopped(LifeCycle event) { + latch.countDown(); + } + }); + // Convenient way to get Jetty to flush its connections, which is required to update the sent/received bytes metrics + server.stop(); + + assertTrue(latch.await(10, SECONDS)); + assertThat(registry.get("jetty.connections.max").gauge().value()).isEqualTo(2.0); + assertThat(registry.get("jetty.connections.request").tag("type", "server").timer().count()) + .isEqualTo(2); + assertThat(registry.get("jetty.connections.bytes.in").summary().totalAmount()).isGreaterThan(1); + } + + @Test + void contributesClientConnectorMetrics() throws Exception { + setup(); + HttpClient httpClient = new HttpClient(); + httpClient.setFollowRedirects(false); + httpClient.addBean(new io.micrometer.binder.jetty.JettyConnectionMetrics(registry)); + + CountDownLatch latch = new CountDownLatch(1); + httpClient.addLifeCycleListener(new LifeCycle.Listener() { + @Override + public void lifeCycleStopped(LifeCycle event) { + latch.countDown(); + } + }); + + httpClient.start(); + + Request post = httpClient.POST("http://localhost:" + connector.getLocalPort()); + post.content(new StringContentProvider("123456")); + post.send(); + httpClient.stop(); + + assertTrue(latch.await(10, SECONDS)); + assertThat(registry.get("jetty.connections.max").gauge().value()).isEqualTo(1.0); + assertThat(registry.get("jetty.connections.request").tag("type", "client").timer().count()) + .isEqualTo(1); + assertThat(registry.get("jetty.connections.bytes.out").summary().totalAmount()).isGreaterThan(1); + } + + @Test + void passingConnectorAddsConnectorNameTag() { + new io.micrometer.binder.jetty.JettyConnectionMetrics(registry, connector); + + assertThat(registry.get("jetty.connections.messages.in").counter().getId().getTag("connector.name")) + .isEqualTo("unnamed"); + } + + @Test + void namedConnectorsGetTaggedWithName() { + connector.setName("super-fast-connector"); + new JettyConnectionMetrics(registry, connector); + + assertThat(registry.get("jetty.connections.messages.in").counter().getId().getTag("connector.name")) + .isEqualTo("super-fast-connector"); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetricsTest.java new file mode 100644 index 0000000000..77ff099449 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettyServerThreadPoolMetricsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.util.Collections; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.binder.jetty.InstrumentedQueuedThreadPool; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Manabu Matsuzaki + */ +class JettyServerThreadPoolMetricsTest { + private SimpleMeterRegistry registry; + private Server server; + + @BeforeEach + void setup() throws Exception { + registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + + Iterable tags = Collections.singletonList(Tag.of("id", "0")); + QueuedThreadPool threadPool = new InstrumentedQueuedThreadPool(registry, tags); + threadPool.setMinThreads(32); + threadPool.setMaxThreads(100); + server = new Server(threadPool); + ServerConnector connector = new ServerConnector(server); + server.setConnectors(new Connector[] { connector }); + server.start(); + } + + @AfterEach + void teardown() throws Exception { + server.stop(); + } + + @Test + void threadMetrics() { + assertThat(registry.get("jetty.threads.config.min").gauge().value()).isEqualTo(32.0); + assertThat(registry.get("jetty.threads.config.max").gauge().value()).isEqualTo(100.0); + assertThat(registry.get("jetty.threads.current").gauge().value()).isNotEqualTo(0.0); + assertThat(registry.get("jetty.threads.busy").gauge().value()).isGreaterThanOrEqualTo(0.0); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettySslHandshakeMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettySslHandshakeMetricsTest.java new file mode 100644 index 0000000000..69b3c25eae --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/JettySslHandshakeMetricsTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 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.binder.jetty; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLSession; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.jetty.JettySslHandshakeMetrics; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.eclipse.jetty.io.ssl.SslHandshakeListener; +import org.eclipse.jetty.server.Connector; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.jetty.JettySslHandshakeMetrics}. + * + * @author John Karp + * @author Johnny Lim + */ +class JettySslHandshakeMetricsTest { + private SimpleMeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private io.micrometer.binder.jetty.JettySslHandshakeMetrics sslHandshakeMetrics; + Iterable tags = Tags.of("id", "0"); + + SSLSession session = mock(SSLSession.class); + SSLEngine engine = mock(SSLEngine.class); + + @BeforeEach + void setup() { + when(engine.getSession()).thenReturn(session); + } + + @Test + void handshakeFailed() { + sslHandshakeMetrics = new io.micrometer.binder.jetty.JettySslHandshakeMetrics(registry, tags); + SslHandshakeListener.Event event = new SslHandshakeListener.Event(engine); + sslHandshakeMetrics.handshakeFailed(event, new javax.net.ssl.SSLHandshakeException("")); + assertThat(registry.get("jetty.ssl.handshakes") + .tags("id", "0", "protocol", "unknown", "ciphersuite", "unknown", "result", "failed") + .counter().count()).isEqualTo(1.0); + } + + @Test + void handshakeSucceeded() { + sslHandshakeMetrics = new io.micrometer.binder.jetty.JettySslHandshakeMetrics(registry, tags); + SslHandshakeListener.Event event = new SslHandshakeListener.Event(engine); + when(session.getProtocol()).thenReturn("TLSv1.3"); + when(session.getCipherSuite()).thenReturn("RSA_XYZZY"); + sslHandshakeMetrics.handshakeSucceeded(event); + assertThat(registry.get("jetty.ssl.handshakes") + .tags("id", "0", "protocol", "TLSv1.3", "ciphersuite", "RSA_XYZZY", "result", "succeeded") + .counter().count()).isEqualTo(1.0); + } + + @Test + void connectorTagAdded() { + Connector connector = mock(Connector.class); + new io.micrometer.binder.jetty.JettySslHandshakeMetrics(registry, connector, tags); + assertThatCode(() -> registry.get("jetty.ssl.handshakes") + .tags("id", "0", "protocol", "unknown", "ciphersuite", "unknown", "result", "failed", + "connector.name", "unnamed") + .counter()).doesNotThrowAnyException(); + } + + @Test + void namedConnectorTagAdded() { + Connector connector = mock(Connector.class); + when(connector.getName()).thenReturn("my-connector"); + new JettySslHandshakeMetrics(registry, connector, tags); + assertThatCode(() -> registry.get("jetty.ssl.handshakes") + .tags("id", "0", "protocol", "unknown", "ciphersuite", "unknown", "result", "failed", + "connector.name", "my-connector") + .counter()).doesNotThrowAnyException(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jetty/TimedHandlerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/TimedHandlerTest.java new file mode 100644 index 0000000000..8387211a63 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jetty/TimedHandlerTest.java @@ -0,0 +1,444 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.http.Outcome; +import io.micrometer.binder.jetty.TimedHandler; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests ported from https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/test/java/org/eclipse/jetty/server/handler/StatisticsHandlerTest.java + */ +class TimedHandlerTest { + private SimpleMeterRegistry registry; + private io.micrometer.binder.jetty.TimedHandler timedHandler; + + private Server server; + private LocalConnector connector; + private LatchHandler latchHandler; + + @BeforeEach + void setup() { + this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + this.timedHandler = new TimedHandler(registry, Tags.empty()); + + this.server = new Server(); + this.connector = new LocalConnector(server); + server.addConnector(connector); + + latchHandler = new LatchHandler(); + + server.setHandler(latchHandler); + latchHandler.setHandler(timedHandler); + } + + @AfterEach + void tearDown() throws Exception { + server.stop(); + server.join(); + } + + @Test + void testRequest() throws Exception { + CyclicBarrier[] barrier = { + new CyclicBarrier(3), + new CyclicBarrier(3) + }; + latchHandler.reset(2); + + timedHandler.setHandler(new AbstractHandler() { + @Override + public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException { + request.setHandled(true); + try { + barrier[0].await(5, TimeUnit.SECONDS); + barrier[1].await(5, TimeUnit.SECONDS); + } catch (Exception x) { + Thread.currentThread().interrupt(); + throw new IOException(x); + } + } + }); + server.start(); + + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + connector.executeRequest(request); + connector.executeRequest(request); + + barrier[0].await(5, TimeUnit.SECONDS); + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()) + .isEqualTo(2); + + barrier[1].await(5, TimeUnit.SECONDS); + assertTrue(latchHandler.await()); + + assertThat(registry.get("jetty.server.requests") + .tag("outcome", Outcome.SUCCESS.name()) + .tag("method", "GET") + .tag("status", "200") + .timer().count()).isEqualTo(2); + } + + @Test + void testSuspendResume() throws Exception { + long dispatchTime = 10; + long requestTime = 50; + AtomicReference asyncHolder = new AtomicReference<>(); + CyclicBarrier[] barrier = { + new CyclicBarrier(2), + new CyclicBarrier(2), + new CyclicBarrier(2) + }; + + timedHandler.setHandler(new AbstractHandler() { + @Override + public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException { + request.setHandled(true); + try { + barrier[0].await(5, TimeUnit.SECONDS); + + Thread.sleep(dispatchTime); + + if (asyncHolder.get() == null) { + asyncHolder.set(request.startAsync()); + } + } catch (Exception x) { + throw new ServletException(x); + } finally { + try { + barrier[1].await(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + } + }); + + server.start(); + + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + connector.executeRequest(request); + + barrier[0].await(5, TimeUnit.SECONDS); + + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()).isEqualTo(1); + + barrier[1].await(5, TimeUnit.SECONDS); + assertTrue(latchHandler.await()); + assertThat(asyncHolder.get()).isNotNull(); + + latchHandler.reset(); + barrier[0].reset(); + barrier[1].reset(); + + Thread.sleep(requestTime); + + asyncHolder.get().addListener(new AsyncListener() { + @Override + public void onTimeout(AsyncEvent event) { + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + + @Override + public void onError(AsyncEvent event) { + } + + @Override + public void onComplete(AsyncEvent event) { + try { + barrier[2].await(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + }); + asyncHolder.get().dispatch(); + + barrier[0].await(5, TimeUnit.SECONDS); // entered app handler + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()).isEqualTo(1); + + barrier[1].await(5, TimeUnit.SECONDS); // exiting app handler + assertTrue(latchHandler.await()); // exited timed handler + + barrier[2].await(5, TimeUnit.SECONDS); // onComplete called + assertThat(registry.get("jetty.server.requests") + .tag("outcome", Outcome.SUCCESS.name()) + .tag("method", "GET") + .tag("status", "200") + .timer().count()).isEqualTo(1); + } + + @Test + void testSuspendExpire() throws Exception { + long dispatchTime = 10; + long timeout = 100; + AtomicReference asyncHolder = new AtomicReference<>(); + CyclicBarrier[] barrier = { + new CyclicBarrier(2), + new CyclicBarrier(2), + new CyclicBarrier(2) + }; + + timedHandler.setHandler(new AbstractHandler() { + @Override + public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException { + request.setHandled(true); + try { + barrier[0].await(5, TimeUnit.SECONDS); + + Thread.sleep(dispatchTime); + + if (asyncHolder.get() == null) { + AsyncContext async = request.startAsync(); + asyncHolder.set(async); + async.setTimeout(timeout); + } + } catch (Exception x) { + throw new ServletException(x); + } finally { + try { + barrier[1].await(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + } + }); + server.start(); + + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + connector.executeRequest(request); + + barrier[0].await(5, TimeUnit.SECONDS); + + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()).isEqualTo(1); + + barrier[1].await(5, TimeUnit.SECONDS); + assertTrue(latchHandler.await()); + assertThat(asyncHolder.get()).isNotNull(); + + asyncHolder.get().addListener(new AsyncListener() { + @Override + public void onTimeout(AsyncEvent event) { + event.getAsyncContext().complete(); + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + + @Override + public void onError(AsyncEvent event) { + } + + @Override + public void onComplete(AsyncEvent event) { + try { + barrier[2].await(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + }); + + barrier[2].await(5, TimeUnit.SECONDS); + + assertThat(registry.get("jetty.server.async.expires").counter().count()).isEqualTo(1); + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()).isEqualTo(0); + } + + @Test + void testSuspendComplete() throws Exception { + long dispatchTime = 10; + AtomicReference asyncHolder = new AtomicReference<>(); + CyclicBarrier[] barrier = { + new CyclicBarrier(2), + new CyclicBarrier(2) + }; + CountDownLatch latch = new CountDownLatch(1); + + timedHandler.setHandler(new AbstractHandler() { + @Override + public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException { + request.setHandled(true); + try { + barrier[0].await(5, TimeUnit.SECONDS); + + Thread.sleep(dispatchTime); + + if (asyncHolder.get() == null) { + AsyncContext async = request.startAsync(); + asyncHolder.set(async); + } + } catch (Exception x) { + throw new ServletException(x); + } finally { + try { + barrier[1].await(5, TimeUnit.SECONDS); + } catch (Exception ignored) { + } + } + } + }); + server.start(); + + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + connector.executeRequest(request); + + barrier[0].await(5, TimeUnit.SECONDS); + assertThat(registry.get("jetty.server.dispatches.open").longTaskTimer().activeTasks()).isEqualTo(1); + + barrier[1].await(5, TimeUnit.SECONDS); + assertTrue(latchHandler.await()); + assertThat(asyncHolder.get()).isNotNull(); + + asyncHolder.get().addListener(new AsyncListener() { + @Override + public void onTimeout(AsyncEvent event) { + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + + @Override + public void onError(AsyncEvent event) { + } + + @Override + public void onComplete(AsyncEvent event) { + try { + latch.countDown(); + } catch (Exception ignored) { + } + } + }); + long requestTime = 20; + Thread.sleep(requestTime); + asyncHolder.get().complete(); + latch.await(5, TimeUnit.SECONDS); + + assertThat(registry.get("jetty.server.requests") + .tag("outcome", Outcome.SUCCESS.name()) + .tag("method", "GET") + .tag("status", "200") + .timer().count()).isEqualTo(1); + } + + @Test + void testAsyncRequestWithShutdown() throws Exception { + long delay = 500; + CountDownLatch serverLatch = new CountDownLatch(1); + timedHandler.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { + AsyncContext asyncContext = request.startAsync(); + asyncContext.setTimeout(0); + new Thread(() -> + { + try { + Thread.sleep(delay); + asyncContext.complete(); + } catch (InterruptedException e) { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500); + asyncContext.complete(); + } + }).start(); + serverLatch.countDown(); + } + }); + server.start(); + + String request = "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "\r\n"; + connector.executeRequest(request); + + assertTrue(serverLatch.await(5, TimeUnit.SECONDS)); + + Future shutdown = timedHandler.shutdown(); + assertFalse(shutdown.isDone()); + + Thread.sleep(delay / 2); + assertFalse(shutdown.isDone()); + + Thread.sleep(delay); + assertTrue(shutdown.isDone()); + } + + private static class LatchHandler extends HandlerWrapper { + private volatile CountDownLatch latch = new CountDownLatch(1); + + @Override + public void handle(String path, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) + throws IOException, ServletException { + try { + super.handle(path, request, httpRequest, httpResponse); + } finally { + latch.countDown(); + } + } + + private void reset() { + reset(1); + } + + private void reset(int count) { + latch = new CountDownLatch(count); + } + + private boolean await() throws InterruptedException { + return latch.await(5, TimeUnit.SECONDS); + } + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ClassLoaderMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ClassLoaderMetricsTest.java new file mode 100644 index 0000000000..188d1c429e --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ClassLoaderMetricsTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 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.binder.jvm; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.jvm.ClassLoaderMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class ClassLoaderMetricsTest { + @Test + void classLoadingMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + new ClassLoaderMetrics().bindTo(registry); + + assertThat(registry.get("jvm.classes.loaded").gauge().value()).isGreaterThan(0); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ExecutorServiceMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ExecutorServiceMetricsTest.java new file mode 100644 index 0000000000..7aef8293ca --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/ExecutorServiceMetricsTest.java @@ -0,0 +1,302 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.search.MeterNotFoundException; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +/** + * Tests for {@link io.micrometer.binder.jvm.ExecutorServiceMetrics}. + * + * @author Clint Checketts + * @author Jon Schneider + * @author Johnny Lim + * @author Sebastian Lövdahl + */ +class ExecutorServiceMetricsTest { + private MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private Iterable userTags = Tags.of("userTagKey", "userTagValue"); + + @DisplayName("Normal executor can be instrumented after being initialized") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void executor(String metricPrefix, String expectedMetricPrefix) throws InterruptedException { + CountDownLatch lock = new CountDownLatch(1); + Executor exec = r -> { + r.run(); + lock.countDown(); + }; + Executor executor = monitorExecutorService("exec", metricPrefix, exec); + executor.execute(() -> System.out.println("hello")); + lock.await(); + + assertThat(registry.get(expectedMetricPrefix + "executor.execution").tags(userTags).tag("name", "exec").timer() + .count()).isEqualTo(1L); + assertThat(registry.get(expectedMetricPrefix + "executor.idle").tags(userTags).tag("name", "exec").timer() + .count()).isEqualTo(1L); + } + + @DisplayName("ExecutorService is casted from Executor when necessary") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void executorCasting(String metricPrefix, String expectedMetricPrefix) { + Executor exec = Executors.newFixedThreadPool(2); + monitorExecutorService("exec", metricPrefix, exec); + assertThreadPoolExecutorMetrics("exec", expectedMetricPrefix); + } + + @DisplayName("thread pool executor can be instrumented after being initialized") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void threadPoolExecutor(String metricPrefix, String expectedMetricPrefix) { + ExecutorService exec = Executors.newFixedThreadPool(2); + monitorExecutorService("exec", metricPrefix, exec); + assertThreadPoolExecutorMetrics("exec", expectedMetricPrefix); + } + + @DisplayName("Scheduled thread pool executor can be instrumented after being initialized") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void scheduledThreadPoolExecutor(String metricPrefix, String expectedMetricPrefix) { + ScheduledExecutorService exec = Executors.newScheduledThreadPool(2); + monitorExecutorService("exec", metricPrefix, exec); + assertThreadPoolExecutorMetrics("exec", expectedMetricPrefix); + } + + @DisplayName("ScheduledExecutorService is casted from Executor when necessary") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void scheduledThreadPoolExecutorAsExecutor(String metricPrefix, String expectedMetricPrefix) { + Executor exec = Executors.newScheduledThreadPool(2); + monitorExecutorService("exec", metricPrefix, exec); + assertThreadPoolExecutorMetrics("exec", expectedMetricPrefix); + } + + @DisplayName("ScheduledExecutorService is casted from ExecutorService when necessary") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void scheduledThreadPoolExecutorAsExecutorService(String metricPrefix, String expectedMetricPrefix) { + ExecutorService exec = Executors.newScheduledThreadPool(2); + monitorExecutorService("exec", metricPrefix, exec); + assertThreadPoolExecutorMetrics("exec", expectedMetricPrefix); + } + + @DisplayName("ExecutorService can be monitored with a default set of metrics") + @DisabledForJreRange(min = JRE.JAVA_16, disabledReason = "See gh-2317 for why we can't run this full test on Java 16+") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void monitorExecutorService(String metricPrefix, String expectedMetricPrefix) throws InterruptedException { + ExecutorService pool = monitorExecutorService("beep.pool", metricPrefix, + Executors.newSingleThreadExecutor()); + CountDownLatch taskStart = new CountDownLatch(1); + CountDownLatch taskComplete = new CountDownLatch(1); + + pool.submit(() -> { + taskStart.countDown(); + assertThat(taskComplete.await(1, TimeUnit.SECONDS)).isTrue(); + System.out.println("beep"); + return 0; + }); + pool.submit(() -> System.out.println("boop")); + + assertThat(taskStart.await(1, TimeUnit.SECONDS)).isTrue(); + + assertThat(registry.get(expectedMetricPrefix + "executor.queued").tags(userTags).tag("name", "beep.pool") + .gauge().value()).isEqualTo(1.0); + + taskComplete.countDown(); + + pool.shutdown(); + assertThat(pool.awaitTermination(1, TimeUnit.SECONDS)).isTrue(); + + assertThat(registry.get(expectedMetricPrefix + "executor").tags(userTags).timer().count()).isEqualTo(2L); + assertThat(registry.get(expectedMetricPrefix + "executor.idle").tags(userTags).timer().count()).isEqualTo(2L); + assertThat(registry.get(expectedMetricPrefix + "executor.queued").tags(userTags).gauge().value()).isEqualTo(0.0); + } + + @DisplayName("No exception thrown trying to monitor Executors private class") + @Test + @Issue("#2447") // Note: only reproduces on Java 16+ or with --illegal-access=deny + void monitorExecutorsExecutorServicePrivateClass() { + assertThatCode(() -> io.micrometer.binder.jvm.ExecutorServiceMetrics.monitor(registry, Executors.newSingleThreadExecutor(), "")) + .doesNotThrowAnyException(); + } + + @DisplayName("ScheduledExecutorService can be monitored with a default set of metrics") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void monitorScheduledExecutorService(String metricPrefix, String expectedMetricPrefix) + throws TimeoutException, ExecutionException, InterruptedException { + ScheduledExecutorService pool = monitorExecutorService("scheduled.pool", metricPrefix, + Executors.newScheduledThreadPool(2)); + + CountDownLatch callableTaskStart = new CountDownLatch(1); + CountDownLatch runnableTaskStart = new CountDownLatch(1); + CountDownLatch callableTaskComplete = new CountDownLatch(1); + CountDownLatch runnableTaskComplete = new CountDownLatch(1); + + Callable scheduledBeepCallable = () -> { + callableTaskStart.countDown(); + assertThat(callableTaskComplete.await(1, TimeUnit.SECONDS)).isTrue(); + return 1; + }; + ScheduledFuture callableResult = pool.schedule(scheduledBeepCallable, 10, + TimeUnit.MILLISECONDS); + + Runnable scheduledBeepRunnable = () -> { + runnableTaskStart.countDown(); + try { + assertThat(runnableTaskComplete.await(1, TimeUnit.SECONDS)).isTrue(); + } catch (InterruptedException e) { + throw new IllegalStateException("scheduled runnable interrupted before completion"); + } + }; + ScheduledFuture runnableResult = pool.schedule(scheduledBeepRunnable, 15, TimeUnit.MILLISECONDS); + + assertThat(registry.get(expectedMetricPrefix + "executor.scheduled.once") + .tags(userTags).tag("name", "scheduled.pool").counter().count()).isEqualTo(2); + + assertThat(callableTaskStart.await(1, TimeUnit.SECONDS)).isTrue(); + assertThat(runnableTaskStart.await(1, TimeUnit.SECONDS)).isTrue(); + + callableTaskComplete.countDown(); + runnableTaskComplete.countDown(); + + pool.shutdown(); + assertThat(pool.awaitTermination(1, TimeUnit.SECONDS)).isTrue(); + + assertThat(callableResult.get(1, TimeUnit.MINUTES)).isEqualTo(1); + assertThat(runnableResult.get(1, TimeUnit.MINUTES)).isNull(); + + assertThat(registry.get(expectedMetricPrefix + "executor").tags(userTags).timer().count()).isEqualTo(2L); + assertThat(registry.get(expectedMetricPrefix + "executor.idle").tags(userTags).timer().count()).isEqualTo(0L); + } + + @DisplayName("ScheduledExecutorService repetitive tasks can be monitored with a default set of metrics") + @ParameterizedTest + @CsvSource({ "custom,custom.", "custom.,custom.", ",''", "' ',''" }) + void monitorScheduledExecutorServiceWithRepetitiveTasks(String metricPrefix, String expectedMetricPrefix) throws InterruptedException { + ScheduledExecutorService pool = monitorExecutorService("scheduled.pool", metricPrefix, + Executors.newScheduledThreadPool(1)); + CountDownLatch fixedRateInvocations = new CountDownLatch(3); + CountDownLatch fixedDelayInvocations = new CountDownLatch(3); + + assertThat(registry.get(expectedMetricPrefix + "executor.scheduled.repetitively").tags(userTags).counter().count()).isEqualTo( + 0); + assertThat(registry.get(expectedMetricPrefix + "executor").tags(userTags).timer().count()).isEqualTo(0L); + + Runnable repeatedAtFixedRate = () -> { + fixedRateInvocations.countDown(); + if (fixedRateInvocations.getCount() == 0) { + throw new RuntimeException("finished execution"); + } + }; + pool.scheduleAtFixedRate(repeatedAtFixedRate, 10, 10, TimeUnit.MILLISECONDS); + + Runnable repeatedWithFixedDelay = () -> { + fixedDelayInvocations.countDown(); + if (fixedDelayInvocations.getCount() == 0) { + throw new RuntimeException("finished execution"); + } + }; + pool.scheduleWithFixedDelay(repeatedWithFixedDelay, 5, 15, TimeUnit.MILLISECONDS); + + assertThat(registry.get(expectedMetricPrefix + "executor.scheduled.repetitively").tags(userTags).counter().count()).isEqualTo( + 2); + + assertThat(fixedRateInvocations.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(fixedDelayInvocations.await(5, TimeUnit.SECONDS)).isTrue(); + + pool.shutdown(); + assertThat(pool.awaitTermination(1, TimeUnit.SECONDS)).isTrue(); + + assertThat(registry.get(expectedMetricPrefix + "executor").tags(userTags).timer().count()).isEqualTo(6L); + assertThat(registry.get(expectedMetricPrefix + "executor.idle").tags(userTags).timer().count()).isEqualTo(0L); + } + + @SuppressWarnings("unchecked") + private T monitorExecutorService(String executorName, String metricPrefix, T exec) { + if (metricPrefix == null) { + return (T) io.micrometer.binder.jvm.ExecutorServiceMetrics.monitor(registry, exec, executorName, userTags); + } else { + return (T) io.micrometer.binder.jvm.ExecutorServiceMetrics.monitor(registry, exec, executorName, metricPrefix, userTags); + } + } + + private void assertThreadPoolExecutorMetrics(String executorName, String metricPrefix) { + registry.get(metricPrefix + "executor.completed").tags(userTags).tag("name", executorName).meter(); + registry.get(metricPrefix + "executor.queued").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.queue.remaining").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.active").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.pool.size").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.pool.core").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.pool.max").tags(userTags).tag("name", executorName).gauge(); + registry.get(metricPrefix + "executor.idle").tags(userTags).tag("name", executorName).timer(); + registry.get(metricPrefix + "executor").tags(userTags).tag("name", executorName).timer(); + } + + @Test + void newSingleThreadScheduledExecutor() { + String executorServiceName = "myExecutorService"; + io.micrometer.binder.jvm.ExecutorServiceMetrics.monitor(registry, Executors.newSingleThreadScheduledExecutor(), executorServiceName); + // timer metrics still available, even on Java 16+ + registry.get("executor").tag("name", executorServiceName).timer(); + if (isJava16OrLater()) return; // see gh-2317; ExecutorServiceMetrics not available for inaccessible JDK internal types + registry.get("executor.completed").tag("name", executorServiceName).functionCounter(); + } + + private boolean isJava16OrLater() { + return JRE.currentVersion().compareTo(JRE.JAVA_16) >= 0; + } + + @Test + void newSingleThreadScheduledExecutorWhenReflectiveAccessIsDisabled() { + String executorServiceName = "myExecutorService"; + io.micrometer.binder.jvm.ExecutorServiceMetrics.disableIllegalReflectiveAccess(); + ExecutorServiceMetrics.monitor(registry, Executors.newSingleThreadScheduledExecutor(), executorServiceName); + registry.get("executor").tag("name", executorServiceName).timer(); + assertThatThrownBy(() -> registry.get("executor.completed").tag("name", executorServiceName).functionCounter()) + .isExactlyInstanceOf(MeterNotFoundException.class); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/GcTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/GcTest.java new file mode 100644 index 0000000000..927de6e99d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/GcTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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.binder.jvm; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Tag("gc") +public @interface GcTest { +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmCompilationMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmCompilationMetricsTest.java new file mode 100644 index 0000000000..616e94bde5 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmCompilationMetricsTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.CompilationMXBean; +import java.lang.management.ManagementFactory; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.jvm.JvmCompilationMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Tests for {@link io.micrometer.binder.jvm.JvmCompilationMetrics}. + */ +class JvmCompilationMetricsTest { + + MeterRegistry registry = new SimpleMeterRegistry(); + + @Test + void compilationTimeMetric() { + new JvmCompilationMetrics().bindTo(registry); + + CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean(); + assumeTrue(compilationMXBean != null && compilationMXBean.isCompilationTimeMonitoringSupported()); + + assertThat(registry.get("jvm.compilation.time").functionCounter().count()).isGreaterThan(0); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmGcMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmGcMetricsTest.java new file mode 100644 index 0000000000..5233868d28 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmGcMetricsTest.java @@ -0,0 +1,195 @@ +/* + * Copyright 2020 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.binder.jvm; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.management.ListenerNotFoundException; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; + +import com.sun.management.GarbageCollectionNotificationInfo; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchRule; +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.Timer; +import io.micrometer.binder.jvm.JvmGcMetrics.GcMetricsNotificationListener; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; +import static org.awaitility.Awaitility.await; + +/** + * Tests for {@link io.micrometer.binder.jvm.JvmGcMetrics}. + * + * @author Johnny Lim + */ +@GcTest +class JvmGcMetricsTest { + + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + io.micrometer.binder.jvm.JvmGcMetrics binder; + + @BeforeEach + void beforeEach() { + this.binder = new JvmGcMetrics(); + binder.bindTo(registry); + } + + @Test + void noJvmImplementationSpecificApiSignatures() { + JavaClasses importedClasses = new ClassFileImporter().importPackages("io.micrometer.binder.jvm"); + + ArchRule noSunManagementInMethodSignatures = methods() + .should().notHaveRawReturnType(resideInAPackage("com.sun.management..")) + .andShould().notHaveRawParameterTypes(DescribedPredicate.anyElementThat(resideInAPackage("com.sun.management.."))); + + noSunManagementInMethodSignatures.check(importedClasses); + } + + @Test + void gcMetricsAvailableAfterGc() { + System.gc(); + await().timeout(200, TimeUnit.MILLISECONDS).alias("NotificationListener takes time after GC") + .untilAsserted(() -> + assertThat(registry.find("jvm.gc.live.data.size").gauge().value()).isPositive()); + assertThat(registry.find("jvm.gc.memory.allocated").counter().count()).isPositive(); + assertThat(registry.find("jvm.gc.max.data.size").gauge().value()).isPositive(); + + if (!binder.isGenerationalGc) { + return; + } + // cannot guarantee an object was promoted, so cannot check for positive count + assertThat(registry.find("jvm.gc.memory.promoted").counter()).isNotNull(); + } + + @Test + @Disabled("Garbage collection can happen before JvmGcMetrics are registered, making our metrics not match overall counts/timings") + // available for some platforms and distributions earlier, but broadest availability in an LTS is 17 + @EnabledForJreRange(min = JRE.JAVA_17) + void gcTimingIsCorrect() { + System.gc(); + long pausePhaseCount = 0; + long pauseTimeMs = 0; + long concurrentPhaseCount = 0; + long concurrentTimeMs = 0; + for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (mbean.getName().contains("Pauses")) { + pausePhaseCount += mbean.getCollectionCount(); + pauseTimeMs += mbean.getCollectionTime(); + } else if (mbean.getName().contains("Cycles")) { + concurrentPhaseCount += mbean.getCollectionCount(); + concurrentTimeMs += mbean.getCollectionTime(); + } + System.out.println(mbean.getName() + " (" + mbean.getCollectionCount() + ") " + mbean.getCollectionTime() + "ms"); + } + checkPhaseCount(pausePhaseCount, concurrentPhaseCount); + checkCollectionTime(pauseTimeMs, concurrentTimeMs); + } + + @Test + @Issue("gh-2872") + void sizeMetricsNotSetToZero() throws InterruptedException { + GcMetricsNotificationListener gcMetricsNotificationListener = binder.gcNotificationListener; + NotificationCapturingListener capturingListener = new NotificationCapturingListener(); + Collection notificationListenerCleanUpRunnables = new ArrayList<>(); + + // register capturing notification listener + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + if (!(gcBean instanceof NotificationEmitter)) { + continue; + } + NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; + notificationEmitter.addNotificationListener(capturingListener, notification -> notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION), null); + notificationListenerCleanUpRunnables.add(() -> { + try { + notificationEmitter.removeNotificationListener(capturingListener); + } catch (ListenerNotFoundException ignore) { + } + }); + } + + try { + // capture real gc notifications + System.gc(); + // reduce flakiness by sleeping to give time for gc notifications to be sent to listeners. + // note: we cannot just wait for any notification because we don't know how many notifications to expect. + // this can still be flawed if we didn't wait for all notifications and proceed with assertions on an + // incomplete set of notifications. + Thread.sleep(100); + List notifications = capturingListener.getNotifications(); + assertThat(notifications).isNotEmpty(); + // replay each notification and check size metrics not set to zero + for (Notification notification : notifications) { + gcMetricsNotificationListener.handleNotification(notification, null); + assertThat(registry.get("jvm.gc.live.data.size").gauge().value()).isPositive(); + assertThat(registry.get("jvm.gc.max.data.size").gauge().value()).isPositive(); + } + } finally { + notificationListenerCleanUpRunnables.forEach(Runnable::run); + } + } + + static class NotificationCapturingListener implements NotificationListener { + + private final List notifications = new ArrayList<>(); + + List getNotifications() { + return Collections.unmodifiableList(notifications); + } + + @Override + public void handleNotification(Notification notification, Object handback) { + notifications.add(notification); + } + } + + private void checkPhaseCount(long expectedPauseCount, long expectedConcurrentCount) { + await().atMost(200, TimeUnit.MILLISECONDS).untilAsserted(() -> { + long observedPauseCount = registry.find("jvm.gc.pause").timers().stream().mapToLong(Timer::count).sum(); + long observedConcurrentCount = registry.find("jvm.gc.concurrent.phase.time").timers().stream().mapToLong(Timer::count).sum(); + assertThat(observedPauseCount).isEqualTo(expectedPauseCount); + assertThat(observedConcurrentCount).isEqualTo(expectedConcurrentCount); + }); + } + + private void checkCollectionTime(long expectedPauseTimeMs, long expectedConcurrentTimeMs) { + await().atMost(200, TimeUnit.MILLISECONDS).untilAsserted(() -> { + double observedPauseTimeMs = registry.find("jvm.gc.pause").timers().stream().mapToDouble(timer -> timer.totalTime(TimeUnit.MILLISECONDS)).sum(); + double observedConcurrentTimeMs = registry.find("jvm.gc.concurrent.phase.time").timers().stream().mapToDouble(timer -> timer.totalTime(TimeUnit.MILLISECONDS)).sum(); + // small difference can happen when less than 1ms timing gets rounded + assertThat(observedPauseTimeMs).isCloseTo(expectedPauseTimeMs, within(1d)); + assertThat(observedConcurrentTimeMs).isCloseTo(expectedConcurrentTimeMs, within(1d)); + }); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmInfoMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmInfoMetricsTest.java new file mode 100644 index 0000000000..df31684fd5 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmInfoMetricsTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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.binder.jvm; + +import java.util.Collection; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.jvm.JvmInfoMetrics; +import io.micrometer.core.instrument.search.Search; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.jvm.JvmInfoMetrics}. + * + * @author Erin Schnabel + */ +class JvmInfoMetricsTest { + + @Test + void assertJvmInfo() { + MeterRegistry registry = new SimpleMeterRegistry(); + new JvmInfoMetrics().bindTo(registry); + + Collection gauges = Search.in(registry).name("jvm.info").gauges(); + assertThat(gauges.size()).isEqualTo(1); + + Gauge jvmInfo = gauges.iterator().next(); + assertThat(jvmInfo.value()).isEqualTo(1L); + + Meter.Id id = jvmInfo.getId(); + assertThat(id.getTag("version")).isNotNull(); + assertThat(id.getTag("vendor")).isNotNull(); + assertThat(id.getTag("runtime")).isNotNull(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryMetricsTest.java new file mode 100644 index 0000000000..ffaa15d63d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryMetricsTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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.binder.jvm; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.BaseUnits; +import io.micrometer.binder.jvm.JvmMemoryMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@code JvmMemoryMetrics}. + * + * @author Michael Weirauch + */ +class JvmMemoryMetricsTest { + + @Test + void memoryMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + new JvmMemoryMetrics().bindTo(registry); + + assertJvmBufferMetrics(registry, "direct"); + assertJvmBufferMetrics(registry, "mapped"); + + assertJvmMemoryMetrics(registry, "heap"); + assertJvmMemoryMetrics(registry, "nonheap"); + } + + private void assertJvmMemoryMetrics(MeterRegistry registry, String area) { + Gauge memUsed = registry.get("jvm.memory.used").tags("area", area).gauge(); + assertThat(memUsed.value()).isGreaterThanOrEqualTo(0); + assertThat(memUsed.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES); + + Gauge memCommitted = registry.get("jvm.memory.committed").tags("area", area).gauge(); + assertThat(memCommitted.value()).isNotNull(); + assertThat(memCommitted.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES); + + Gauge memMax = registry.get("jvm.memory.max").tags("area", area).gauge(); + assertThat(memMax.value()).isNotNull(); + assertThat(memMax.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES); + } + + private void assertJvmBufferMetrics(MeterRegistry registry, String bufferId) { + assertThat(registry.get("jvm.buffer.count").tags("id", bufferId) + .gauge().value()).isGreaterThanOrEqualTo(0); + + Gauge memoryUsedDirect = registry.get("jvm.buffer.memory.used").tags("id", bufferId).gauge(); + assertThat(memoryUsedDirect.value()).isNotNull(); + assertThat(memoryUsedDirect.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES); + + Gauge bufferTotal = registry.get("jvm.buffer.total.capacity").tags("id", bufferId).gauge(); + assertThat(bufferTotal.value()).isGreaterThanOrEqualTo(0); + assertThat(bufferTotal.getId().getBaseUnit()).isEqualTo(BaseUnits.BYTES); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryTest.java new file mode 100644 index 0000000000..8b14aa4006 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmMemoryTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 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.binder.jvm; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@GcTest +class JvmMemoryTest { + @Test + void getLongLivedHeapPool() { + assertThat(io.micrometer.binder.jvm.JvmMemory.getLongLivedHeapPools()).isNotEmpty(); + } + + @Test + void assertTolerateNullName() { + // There is a way for the name passed to these methods to be null. + // Ensure they don't fail; + assertThat(io.micrometer.binder.jvm.JvmMemory.isLongLivedPool(null)).isFalse(); + assertThat(io.micrometer.binder.jvm.JvmMemory.isAllocationPool(null)).isFalse(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmThreadMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmThreadMetricsTest.java new file mode 100644 index 0000000000..a7bbed0844 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/jvm/JvmThreadMetricsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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.binder.jvm; + +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.jvm.JvmThreadMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link io.micrometer.binder.jvm.JvmThreadMetrics}. + * + * @author Jon Schneider + * @author Johnny Lim + */ +class JvmThreadMetricsTest { + @Test + void threadMetrics() { + MeterRegistry registry = new SimpleMeterRegistry(); + new io.micrometer.binder.jvm.JvmThreadMetrics().bindTo(registry); + + assertThat(registry.get("jvm.threads.live").gauge().value()).isGreaterThan(0); + assertThat(registry.get("jvm.threads.daemon").gauge().value()).isGreaterThan(0); + assertThat(registry.get("jvm.threads.peak").gauge().value()).isGreaterThan(0); + assertThat(registry.get("jvm.threads.states").tag("state", "runnable").gauge().value()).isGreaterThan(0); + + createBlockedThread(); + assertThat(registry.get("jvm.threads.states").tag("state", "blocked").gauge().value()).isGreaterThan(0); + assertThat(registry.get("jvm.threads.states").tag("state", "waiting").gauge().value()).isGreaterThan(0); + + createTimedWaitingThread(); + assertThat(registry.get("jvm.threads.states").tag("state", "timed-waiting").gauge().value()).isGreaterThan(0); + } + + @Test + void getThreadStateCountWhenThreadInfoIsNullShouldWork() { + ThreadMXBean threadBean = mock(ThreadMXBean.class); + long[] threadIds = {1L, 2L}; + when(threadBean.getAllThreadIds()).thenReturn(threadIds); + ThreadInfo threadInfo = mock(ThreadInfo.class); + when(threadInfo.getThreadState()).thenReturn(Thread.State.RUNNABLE); + when(threadBean.getThreadInfo(threadIds)).thenReturn(new ThreadInfo[] { threadInfo, null }); + assertThat(JvmThreadMetrics.getThreadStateCount(threadBean, Thread.State.RUNNABLE)).isEqualTo(1); + } + + private void createTimedWaitingThread() { + new Thread(() -> { + sleep(5); + }).start(); + sleep(1); + } + + private void sleep(int seconds) { + try { + TimeUnit.SECONDS.sleep(seconds); + } + catch (InterruptedException ignored) { + } + } + + private void createBlockedThread() { + Object lock = new Object(); + new Thread(() -> { + synchronized (lock) { + sleep(5); + } + }).start(); + new Thread(() -> { + synchronized (lock) { + sleep(5); + } + }).start(); + sleep(1); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsAdminTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsAdminTest.java new file mode 100644 index 0000000000..451a7b4855 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsAdminTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.util.Properties; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.kafka.KafkaClientMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.clients.admin.AdminClient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; +import static org.apache.kafka.clients.admin.AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG; +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaClientMetricsAdminTest { + private static final String BOOTSTRAP_SERVERS = "localhost:9092"; + private Tags tags = Tags.of("app", "myapp", "version", "1"); + io.micrometer.binder.kafka.KafkaClientMetrics metrics; + + @AfterEach + void afterEach() { + if (metrics != null) + metrics.close(); + } + + @Test void shouldCreateMeters() { + try (AdminClient adminClient = createAdmin()) { + metrics = new io.micrometer.binder.kafka.KafkaClientMetrics(adminClient); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getName()) + .allMatch(s -> s.startsWith(METRIC_NAME_PREFIX)); + } + } + + @Test void shouldCreateMetersWithTags() { + try (AdminClient adminClient = createAdmin()) { + metrics = new KafkaClientMetrics(adminClient, tags); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + } + } + + private AdminClient createAdmin() { + Properties adminConfig = new Properties(); + adminConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + return AdminClient.create(adminConfig); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsConsumerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsConsumerTest.java new file mode 100644 index 0000000000..5ba3f8e310 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsConsumerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.util.Properties; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.kafka.KafkaClientMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; +import static org.apache.kafka.clients.consumer.ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.GROUP_ID_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG; +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaClientMetricsConsumerTest { + private static final String BOOTSTRAP_SERVERS = "localhost:9092"; + private Tags tags = Tags.of("app", "myapp", "version", "1"); + io.micrometer.binder.kafka.KafkaClientMetrics metrics; + + @AfterEach + void afterEach() { + if (metrics != null) + metrics.close(); + } + + @Test void shouldCreateMeters() { + try (Consumer consumer = createConsumer()) { + metrics = new io.micrometer.binder.kafka.KafkaClientMetrics(consumer); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getName()) + .allMatch(s -> s.startsWith(METRIC_NAME_PREFIX)); + } + } + + @Test void shouldCreateMetersWithTags() { + try (Consumer consumer = createConsumer()) { + metrics = new KafkaClientMetrics(consumer, tags); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + } + } + + private Consumer createConsumer() { + Properties consumerConfig = new Properties(); + consumerConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + consumerConfig.put(KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerConfig.put(VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerConfig.put(GROUP_ID_CONFIG, "group"); + return new KafkaConsumer<>(consumerConfig); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsIntegrationTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsIntegrationTest.java new file mode 100644 index 0000000000..5c457f2376 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsIntegrationTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.time.Duration; +import java.util.Collections; +import java.util.Properties; + +import io.micrometer.binder.kafka.KafkaClientMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static java.lang.System.out; +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@Tag("docker") +class KafkaClientMetricsIntegrationTest { + @Container + private KafkaContainer kafkaContainer = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.1")); + + @Test + void shouldManageProducerAndConsumerMetrics() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + + assertThat(registry.getMeters()).hasSize(0); + + Properties producerConfigs = new Properties(); + producerConfigs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers()); + Producer producer = new KafkaProducer<>( + producerConfigs, new StringSerializer(), new StringSerializer()); + + io.micrometer.binder.kafka.KafkaClientMetrics producerKafkaMetrics = new io.micrometer.binder.kafka.KafkaClientMetrics(producer); + producerKafkaMetrics.bindTo(registry); + + int producerMetrics = registry.getMeters().size(); + assertThat(registry.getMeters()).hasSizeGreaterThan(0); + assertThat(registry.getMeters()) + .extracting(m -> m.getId().getTag("kafka.version")) + .allMatch(v -> !v.isEmpty()); + + Properties consumerConfigs = new Properties(); + consumerConfigs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers()); + consumerConfigs.put(ConsumerConfig.GROUP_ID_CONFIG, "test"); + Consumer consumer = new KafkaConsumer<>( + consumerConfigs, new StringDeserializer(), new StringDeserializer()); + + io.micrometer.binder.kafka.KafkaClientMetrics consumerKafkaMetrics = new io.micrometer.binder.kafka.KafkaClientMetrics(consumer); + consumerKafkaMetrics.bindTo(registry); + + //Printing out for discovery purposes + out.println("Meters from producer before sending:"); + printMeters(registry); + + int producerAndConsumerMetrics = registry.getMeters().size(); + assertThat(registry.getMeters()).hasSizeGreaterThan(producerMetrics); + assertThat(registry.getMeters()) + .extracting(m -> m.getId().getTag("kafka.version")) + .allMatch(v -> !v.isEmpty()); + + String topic = "test"; + producer.send(new ProducerRecord<>(topic, "key", "value")); + producer.flush(); + + //Printing out for discovery purposes + out.println("Meters from producer after sending and consumer before poll:"); + printMeters(registry); + + producerKafkaMetrics.checkAndBindMetrics(registry); + + int producerAndConsumerMetricsAfterSend = registry.getMeters().size(); + assertThat(registry.getMeters()).hasSizeGreaterThan(producerAndConsumerMetrics); + assertThat(registry.getMeters()) + .extracting(m -> m.getId().getTag("kafka.version")) + .allMatch(v -> !v.isEmpty()); + + consumer.subscribe(Collections.singletonList(topic)); + + consumer.poll(Duration.ofMillis(100)); + + //Printing out for discovery purposes + out.println("Meters from producer and consumer after polling:"); + printMeters(registry); + + consumerKafkaMetrics.checkAndBindMetrics(registry); + + assertThat(registry.getMeters()).hasSizeGreaterThan(producerAndConsumerMetricsAfterSend); + assertThat(registry.getMeters()) + .extracting(m -> m.getId().getTag("kafka.version")) + .allMatch(v -> !v.isEmpty()); + + //Printing out for discovery purposes + out.println("All meters from producer and consumer:"); + printMeters(registry); + + producerKafkaMetrics.close(); + consumerKafkaMetrics.close(); + } + + @Test void shouldRegisterMetricsFromDifferentClients() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + + assertThat(registry.getMeters()).hasSize(0); + + Properties producer1Configs = new Properties(); + producer1Configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers()); + producer1Configs.put(ProducerConfig.CLIENT_ID_CONFIG, "producer1"); + Producer producer1 = new KafkaProducer<>( + producer1Configs, new StringSerializer(), new StringSerializer()); + + io.micrometer.binder.kafka.KafkaClientMetrics producer1KafkaMetrics = new io.micrometer.binder.kafka.KafkaClientMetrics(producer1); + producer1KafkaMetrics.bindTo(registry); + + int producer1Metrics = registry.getMeters().size(); + assertThat(producer1Metrics).isGreaterThan(0); + + producer1.send(new ProducerRecord<>("topic1", "foo")); + producer1.flush(); + + producer1KafkaMetrics.checkAndBindMetrics(registry); + + int producer1MetricsAfterSend = registry.getMeters().size(); + assertThat(producer1MetricsAfterSend).isGreaterThan(producer1Metrics); + + Properties producer2Configs = new Properties(); + producer2Configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, + kafkaContainer.getBootstrapServers()); + producer2Configs.put(ProducerConfig.CLIENT_ID_CONFIG, "producer2"); + Producer producer2 = new KafkaProducer<>( + producer2Configs, new StringSerializer(), new StringSerializer()); + + io.micrometer.binder.kafka.KafkaClientMetrics producer2KafkaMetrics = new KafkaClientMetrics(producer2); + producer2KafkaMetrics.bindTo(registry); + + producer2.send(new ProducerRecord<>("topic1", "foo")); + producer2.flush(); + + producer2KafkaMetrics.checkAndBindMetrics(registry); + + int producer2MetricsAfterSend = registry.getMeters().size(); + assertThat(producer2MetricsAfterSend).isEqualTo(producer1MetricsAfterSend * 2); + } + + void printMeters(SimpleMeterRegistry registry) { + registry.getMeters().forEach(meter -> out.println(meter.getId() + " => " + meter.measure())); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsProducerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsProducerTest.java new file mode 100644 index 0000000000..99cffea6c0 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaClientMetricsProducerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.util.Properties; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.kafka.KafkaClientMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.kafka.KafkaClientMetrics.METRIC_NAME_PREFIX; +import static org.apache.kafka.clients.producer.ProducerConfig.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaClientMetricsProducerTest { + private static final String BOOTSTRAP_SERVERS = "localhost:9092"; + private Tags tags = Tags.of("app", "myapp", "version", "1"); + io.micrometer.binder.kafka.KafkaClientMetrics metrics; + + @AfterEach + void afterEach() { + if (metrics != null) + metrics.close(); + } + + @Test void shouldCreateMeters() { + try (Producer producer = createProducer()) { + metrics = new io.micrometer.binder.kafka.KafkaClientMetrics(producer); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getName()) + .allMatch(s -> s.startsWith(METRIC_NAME_PREFIX)); + } + } + + @Test void shouldCreateMetersWithTags() { + try (Producer producer = createProducer()) { + metrics = new KafkaClientMetrics(producer, tags); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + } + } + + private Producer createProducer() { + Properties producerConfig = new Properties(); + producerConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + producerConfig.put(KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerConfig.put(VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new KafkaProducer<>(producerConfig); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaMetricsTest.java new file mode 100644 index 0000000000..0e24c1ed8d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaMetricsTest.java @@ -0,0 +1,559 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.Measurement; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.common.Metric; +import org.apache.kafka.common.MetricName; +import org.apache.kafka.common.metrics.KafkaMetric; +import org.apache.kafka.common.metrics.MetricConfig; +import org.apache.kafka.common.metrics.stats.Value; +import org.apache.kafka.common.utils.Time; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaMetricsTest { + private io.micrometer.binder.kafka.KafkaMetrics kafkaMetrics; + + @AfterEach + void afterEach() { + if (kafkaMetrics != null) + kafkaMetrics.close(); + } + + @Test + void shouldKeepMetersWhenMetricsDoNotChange() { + //Given + Supplier> supplier = () -> { + MetricName metricName = new MetricName("a", "b", "c", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(metricName, metric); + }; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + } + + @Test + void closeShouldRemoveAllMeters() { + //Given + Supplier> supplier = () -> { + MetricName metricName = new MetricName("a", "b", "c", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(metricName, metric); + }; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetrics.close(); + assertThat(registry.getMeters()).isEmpty(); + } + + @Test + void shouldAddNewMetersWhenMetricsChange() { + //Given + AtomicReference> metrics = new AtomicReference<>(new LinkedHashMap<>()); + Supplier> supplier = () -> metrics.updateAndGet(map -> { + MetricName metricName = new MetricName("a0", "b0", "c0", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + map.put(metricName, metric); + return map; + }); + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + metrics.updateAndGet(map -> { + MetricName metricName = new MetricName("a1", "b1", "c1", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + map.put(metricName, metric); + return map; + }); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(2); + } + + @Test + void shouldNotAddAppInfoMetrics() { + Supplier> supplier = () -> { + Map metrics = new LinkedHashMap<>(); + MetricName metricName = new MetricName("a0", "b0", "c0", new LinkedHashMap<>()); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + metrics.put(metricName, metric); + MetricName appInfoMetricName = + new MetricName("a1", io.micrometer.binder.kafka.KafkaMetrics.METRIC_GROUP_APP_INFO, "c0", + new LinkedHashMap<>()); + KafkaMetric appInfoMetric = + new KafkaMetric(this, appInfoMetricName, new Value(), new MetricConfig(), Time.SYSTEM); + metrics.put(appInfoMetricName, appInfoMetric); + return metrics; + }; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + } + + @Test + void shouldRemoveOlderMeterWithLessTags() { + Map tags = new LinkedHashMap<>(); + Supplier> supplier = () -> { + MetricName metricName = new MetricName("a", "b", "c", tags); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(metricName, metric); + }; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getTags()).hasSize(1); //only version + + tags.put("key0", "value0"); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getTags()).hasSize(2); + } + + @Test + void shouldRemoveMeterWithLessTags() { + Supplier> supplier = () -> { + MetricName firstName = new MetricName("a", "b", "c", Collections.emptyMap()); + KafkaMetric firstMetric = new KafkaMetric(this, firstName, new Value(), new MetricConfig(), Time.SYSTEM); + Map tags = new LinkedHashMap<>(); + tags.put("key0", "value0"); + MetricName secondName = new MetricName("a", "b", "c", tags); + KafkaMetric secondMetric = new KafkaMetric(this, secondName, new Value(), new MetricConfig(), Time.SYSTEM); + Map metrics = new LinkedHashMap<>(); + metrics.put(firstName, firstMetric); + metrics.put(secondName, secondMetric); + return metrics; + }; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getTags()).hasSize(2); // version + key0 + } + + @Test + void shouldBindMetersWithSameTags() { + Supplier> supplier = () -> { + Map firstTags = new LinkedHashMap<>(); + firstTags.put("key0", "value0"); + MetricName firstName = new MetricName("a", "b", "c", firstTags); + KafkaMetric firstMetric = new KafkaMetric(this, firstName, new Value(), new MetricConfig(), Time.SYSTEM); + Map secondTags = new LinkedHashMap<>(); + secondTags.put("key0", "value1"); + MetricName secondName = new MetricName("a", "b", "c", secondTags); + KafkaMetric secondMetric = new KafkaMetric(this, secondName, new Value(), new MetricConfig(), Time.SYSTEM); + + Map metrics = new LinkedHashMap<>(); + metrics.put(firstName, firstMetric); + metrics.put(secondName, secondMetric); + return metrics; + }; + + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(2); + assertThat(registry.getMeters().get(0).getId().getTags()).hasSize(2); // version + key0 + } + + @Issue("#1968") + @Test + void shouldBindMetersWithDifferentClientIds() { + Supplier> supplier = () -> { + Map firstTags = new LinkedHashMap<>(); + firstTags.put("key0", "value0"); + firstTags.put("client-id", "client0"); + MetricName firstName = new MetricName("a", "b", "c", firstTags); + KafkaMetric firstMetric = new KafkaMetric(this, firstName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(firstName, firstMetric); + }; + + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + registry.counter("kafka.b.a", "client-id", "client1", "key0", "value0", "kafka-version", "unknown"); + //When + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(2); + } + + @Issue("#1968") + @Test + void shouldRemoveOlderMeterWithLessTagsWhenCommonTagsConfigured() { + //Given + Map tags = new LinkedHashMap<>(); + Supplier> supplier = () -> { + MetricName metricName = new MetricName("a", "b", "c", tags); + KafkaMetric metric = new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + return Collections.singletonMap(metricName, metric); + }; + + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + registry.config().commonTags("common", "value"); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getTags()).containsExactlyInAnyOrder(Tag.of("kafka.version", "unknown"), Tag.of("common", "value")); // only version + + tags.put("key0", "value0"); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getTags()).containsExactlyInAnyOrder(Tag.of("kafka.version", "unknown"), Tag.of("key0", "value0"), Tag.of("common", "value")); + } + + @Issue("#2212") + @Test + void shouldRemoveMeterWithLessTagsWithMultipleClients() { + //Given + AtomicReference> metrics = new AtomicReference<>(new LinkedHashMap<>()); + Supplier> supplier = () -> metrics.updateAndGet(map -> { + Map firstTags = new LinkedHashMap<>(); + Map secondTags = new LinkedHashMap<>(); + firstTags.put("key0", "value0"); + firstTags.put("client-id", "client0"); + secondTags.put("key0", "value0"); + secondTags.put("client-id", "client1"); + MetricName firstName = new MetricName("a", "b", "c", firstTags); + MetricName secondName = new MetricName("a", "b", "c", secondTags); + KafkaMetric firstMetric = new KafkaMetric(this, firstName, new Value(), new MetricConfig(), Time.SYSTEM); + KafkaMetric secondMetric = new KafkaMetric(this, secondName, new Value(), new MetricConfig(), Time.SYSTEM); + + map.put(firstName, firstMetric); + map.put(secondName, secondMetric); + return map; + }); + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + // simulate PrometheusMeterRegistry restriction + registry.config() + .onMeterAdded(meter -> registry.find(meter.getId().getName()).meters().stream() + .filter(m -> m.getId().getTags().size() != meter.getId().getTags().size()) + .findAny().ifPresent(m -> { + throw new RuntimeException("meter exists with different number of tags"); + })); + //When + kafkaMetrics.bindTo(registry); + //Then + assertThat(registry.getMeters()).hasSize(2); + // Given + metrics.updateAndGet(map -> { + Map firstTags = new LinkedHashMap<>(); + Map secondTags = new LinkedHashMap<>(); + firstTags.put("key0", "value0"); + firstTags.put("client-id", "client0"); + secondTags.put("key0", "value0"); + secondTags.put("client-id", "client1"); + // more tags than before + firstTags.put("key1", "value1"); + secondTags.put("key1", "value1"); + + MetricName firstName = new MetricName("a", "b", "c", firstTags); + MetricName secondName = new MetricName("a", "b", "c", secondTags); + KafkaMetric firstMetric = new KafkaMetric(this, firstName, new Value(), new MetricConfig(), Time.SYSTEM); + KafkaMetric secondMetric = new KafkaMetric(this, secondName, new Value(), new MetricConfig(), Time.SYSTEM); + + map.put(firstName, firstMetric); + map.put(secondName, secondMetric); + return map; + }); + // When + kafkaMetrics.checkAndBindMetrics(registry); + // Then + assertThat(registry.getMeters()).hasSize(2); + registry.getMeters().forEach(meter -> assertThat(meter.getId().getTags()) + .extracting(Tag::getKey).containsOnly("key0", "key1", "client.id", "kafka.version")); + } + + @Issue("#2726") + @Test + void shouldUseMetricFromSupplierIfInstanceChanges() { + MetricName metricName = new MetricName("a0", "b0", "c0", new LinkedHashMap<>()); + Value oldValue = new Value(); + oldValue.record(new MetricConfig(), 1.0, System.currentTimeMillis()); + KafkaMetric oldMetricInstance = new KafkaMetric(this, metricName, oldValue, new MetricConfig(), Time.SYSTEM); + + Map metrics = new HashMap<>(); + metrics.put(metricName, oldMetricInstance); + + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(() -> metrics); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + Value newValue = new Value(); + newValue.record(new MetricConfig(), 2.0, System.currentTimeMillis()); + KafkaMetric newMetricInstance = new KafkaMetric(this, metricName, newValue, new MetricConfig(), Time.SYSTEM); + metrics.put(metricName, newMetricInstance); + + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()) + .singleElement() + .extracting(Meter::measure) + .satisfies(measurements -> + assertThat(measurements) + .singleElement() + .extracting(Measurement::getValue) + .isEqualTo(2.0) + ); + } + + @Issue("#2801") + @Test + void shouldUseMetricFromSupplierIndirectly() { + AtomicReference> metricsReference = new AtomicReference<>(new HashMap<>()); + + MetricName oldMetricName = new MetricName("a0", "b0", "c0", new LinkedHashMap<>()); + Value oldValue = new Value(); + oldValue.record(new MetricConfig(), 1.0, System.currentTimeMillis()); + KafkaMetric oldMetricInstance = new KafkaMetric(this, oldMetricName, oldValue, new MetricConfig(), Time.SYSTEM); + + metricsReference.get().put(oldMetricName, oldMetricInstance); + + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(metricsReference::get); + MeterRegistry registry = new SimpleMeterRegistry(); + + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(1); + + assertThat(registry.getMeters()) + .singleElement() + .extracting(Meter::measure) + .satisfies(measurements -> + assertThat(measurements) + .singleElement() + .extracting(Measurement::getValue) + .isEqualTo(1.0) + ); + + metricsReference.set(new HashMap<>()); + MetricName newMetricName = new MetricName("a0", "b0", "c0", new LinkedHashMap<>()); + Value newValue = new Value(); + newValue.record(new MetricConfig(), 2.0, System.currentTimeMillis()); + KafkaMetric newMetricInstance = new KafkaMetric(this, newMetricName, newValue, new MetricConfig(), Time.SYSTEM); + metricsReference.get().put(newMetricName, newMetricInstance); + + assertThat(registry.getMeters()) + .singleElement() + .extracting(Meter::measure) + .satisfies(measurements -> + assertThat(measurements) + .singleElement() + .extracting(Measurement::getValue) + .isEqualTo(1.0) + ); // still referencing the old value since the map is only updated in checkAndBindMetrics + + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()) + .singleElement() + .extracting(Meter::measure) + .satisfies(measurements -> + assertThat(measurements) + .singleElement() + .extracting(Measurement::getValue) + .isEqualTo(2.0) + ); // referencing the new value since the map was updated in checkAndBindMetrics + } + + @Issue("#2843") + @Test + void shouldRemoveOldMeters() { + Map kafkaMetricMap = new HashMap<>(); + Supplier> supplier = () -> kafkaMetricMap; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + registry.config().commonTags("commonTest", "42"); + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(0); + + MetricName aMetric = createMetricName("a"); + MetricName bMetric = createMetricName("b"); + + kafkaMetricMap.put(aMetric, createKafkaMetric(aMetric)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + + kafkaMetricMap.put(aMetric, createKafkaMetric(aMetric)); + kafkaMetricMap.put(bMetric, createKafkaMetric(bMetric)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(2); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + + kafkaMetricMap.put(aMetric, createKafkaMetric(aMetric)); + kafkaMetricMap.put(bMetric, createKafkaMetric(bMetric)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(2); + + kafkaMetricMap.remove(bMetric); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getName()).isEqualTo("kafka.test.a"); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + } + + @Issue("#2843") + @Test + void shouldRemoveOldMetersWithTags() { + Map kafkaMetricMap = new HashMap<>(); + Supplier> supplier = () -> kafkaMetricMap; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + registry.config().commonTags("commonTest", "42"); + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(0); + + MetricName aMetricV1 = createMetricName("a", "foo", "v1"); + MetricName aMetricV2 = createMetricName("a", "foo", "v2"); + MetricName bMetric = createMetricName("b", "foo", "n/a"); + + kafkaMetricMap.put(aMetricV1, createKafkaMetric(aMetricV1)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + + kafkaMetricMap.put(aMetricV1, createKafkaMetric(aMetricV1)); + kafkaMetricMap.put(aMetricV2, createKafkaMetric(aMetricV2)); + kafkaMetricMap.put(bMetric, createKafkaMetric(bMetric)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(3); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + + kafkaMetricMap.put(aMetricV1, createKafkaMetric(aMetricV1)); + kafkaMetricMap.put(aMetricV2, createKafkaMetric(aMetricV2)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(2); + + kafkaMetricMap.remove(aMetricV1); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + assertThat(registry.getMeters().get(0).getId().getName()).isEqualTo("kafka.test.a"); + assertThat(registry.getMeters().get(0).getId().getTags()).containsExactlyInAnyOrder( + Tag.of("commonTest", "42"), + Tag.of("kafka.version", "unknown"), + Tag.of("foo", "v2") + ); + + kafkaMetricMap.clear(); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + } + + @Issue("#2879") + @Test + void removeShouldWorkForNonExistingMeters() { + Map kafkaMetricMap = new HashMap<>(); + Supplier> supplier = () -> kafkaMetricMap; + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(supplier); + MeterRegistry registry = new SimpleMeterRegistry(); + kafkaMetrics.bindTo(registry); + assertThat(registry.getMeters()).hasSize(0); + + MetricName aMetric = createMetricName("a"); + kafkaMetricMap.put(aMetric, createKafkaMetric(aMetric)); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(1); + + kafkaMetricMap.clear(); + registry.forEachMeter(registry::remove); + kafkaMetrics.checkAndBindMetrics(registry); + assertThat(registry.getMeters()).hasSize(0); + } + + @Issue("#2879") + @Test + void checkAndBindMetricsShouldNotFail() { + kafkaMetrics = new io.micrometer.binder.kafka.KafkaMetrics(() -> { throw new RuntimeException("simulated"); }); + MeterRegistry registry = new SimpleMeterRegistry(); + kafkaMetrics.checkAndBindMetrics(registry); + } + + private MetricName createMetricName(String name) { + return createMetricName(name, Collections.emptyMap()); + } + + private MetricName createMetricName(String name, String... keyValues) { + if (keyValues.length % 2 == 1) { + throw new IllegalArgumentException("size must be even, it is a set of key=value pairs"); + } + else { + Map tagsMap = new HashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + tagsMap.put(keyValues[i], keyValues[i + 1]); + } + + return createMetricName(name, tagsMap); + } + } + + private MetricName createMetricName(String name, Map tags) { + return new MetricName(name, "test", "for testing", tags); + } + + private KafkaMetric createKafkaMetric(MetricName metricName) { + return new KafkaMetric(this, metricName, new Value(), new MetricConfig(), Time.SYSTEM); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaStreamsMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaStreamsMetricsTest.java new file mode 100644 index 0000000000..1f98b11a17 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/kafka/KafkaStreamsMetricsTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 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.binder.kafka; + +import java.util.Properties; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.kafka.KafkaStreamsMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.StreamsBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static io.micrometer.binder.kafka.KafkaStreamsMetrics.METRIC_NAME_PREFIX; +import static org.apache.kafka.streams.StreamsConfig.APPLICATION_ID_CONFIG; +import static org.apache.kafka.streams.StreamsConfig.BOOTSTRAP_SERVERS_CONFIG; +import static org.assertj.core.api.Assertions.assertThat; + +class KafkaStreamsMetricsTest { + private static final String BOOTSTRAP_SERVERS = "localhost:9092"; + private Tags tags = Tags.of("app", "myapp", "version", "1"); + io.micrometer.binder.kafka.KafkaStreamsMetrics metrics; + + @AfterEach + void afterEach() { + if (metrics != null) + metrics.close(); + } + + @Test void shouldCreateMeters() { + try (KafkaStreams kafkaStreams = createStreams()) { + metrics = new io.micrometer.binder.kafka.KafkaStreamsMetrics(kafkaStreams); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getName()) + .allMatch(s -> s.startsWith(METRIC_NAME_PREFIX)); + } + } + + @Test void shouldCreateMetersWithTags() { + try (KafkaStreams kafkaStreams = createStreams()) { + metrics = new KafkaStreamsMetrics(kafkaStreams, tags); + MeterRegistry registry = new SimpleMeterRegistry(); + + metrics.bindTo(registry); + + assertThat(registry.getMeters()) + .hasSizeGreaterThan(0) + .extracting(meter -> meter.getId().getTag("app")) + .allMatch(s -> s.equals("myapp")); + } + } + + private KafkaStreams createStreams() { + StreamsBuilder builder = new StreamsBuilder(); + builder.stream("input").to("output"); + Properties streamsConfig = new Properties(); + streamsConfig.put(BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); + streamsConfig.put(APPLICATION_ID_CONFIG, "app"); + return new KafkaStreams(builder.build(), streamsConfig); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/logging/Log4j2MetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/logging/Log4j2MetricsTest.java new file mode 100644 index 0000000000..152284744d --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/logging/Log4j2MetricsTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2017 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.binder.logging; + +import java.io.IOException; +import java.time.Duration; + +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Tests for {@link io.micrometer.binder.logging.Log4j2Metrics}. + * + * @author Steven Sheehy + * @author Johnny Lim + */ +class Log4j2MetricsTest { + + private final MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + + @BeforeEach + void cleanUp() { + LogManager.shutdown(); + } + + @AfterAll + static void cleanUpAfterAll() { + LogManager.shutdown(); + } + + @Test + void log4j2LevelMetrics() { + new io.micrometer.binder.logging.Log4j2Metrics().bindTo(registry); + + assertThat(registry.get("log4j2.events").counter().count()).isEqualTo(0.0); + + Logger logger = LogManager.getLogger(Log4j2MetricsTest.class); + Configurator.setLevel(Log4j2MetricsTest.class.getName(), Level.INFO); + logger.info("info"); + logger.warn("warn"); + logger.fatal("fatal"); + logger.error("error"); + logger.debug("debug"); // shouldn't record a metric as per log level config + logger.trace("trace"); // shouldn't record a metric as per log level config + + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1.0); + assertThat(registry.get("log4j2.events").tags("level", "warn").counter().count()).isEqualTo(1.0); + assertThat(registry.get("log4j2.events").tags("level", "fatal").counter().count()).isEqualTo(1.0); + assertThat(registry.get("log4j2.events").tags("level", "error").counter().count()).isEqualTo(1.0); + assertThat(registry.get("log4j2.events").tags("level", "debug").counter().count()).isEqualTo(0.0); + assertThat(registry.get("log4j2.events").tags("level", "trace").counter().count()).isEqualTo(0.0); + } + + @Test + void filterWhenLoggerAdditivityIsFalseShouldWork() { + Logger additivityDisabledLogger = LogManager.getLogger("additivityDisabledLogger"); + Configurator.setLevel("additivityDisabledLogger", Level.INFO); + + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(); + Configuration configuration = loggerContext.getConfiguration(); + LoggerConfig loggerConfig = configuration.getLoggerConfig("additivityDisabledLogger"); + loggerConfig.setAdditive(false); + + new io.micrometer.binder.logging.Log4j2Metrics().bindTo(registry); + + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(0); + + additivityDisabledLogger.info("Hello, world!"); + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1); + } + + @Issue("#1466") + @Test + void filterWhenRootLoggerAdditivityIsFalseShouldWork() throws IOException { + ConfigurationSource source = new ConfigurationSource(getClass().getResourceAsStream("/binder/logging/log4j2-root-logger-additivity-false.xml")); + Configurator.initialize(null, source); + + Logger logger = LogManager.getLogger(Log4j2MetricsTest.class); + + new io.micrometer.binder.logging.Log4j2Metrics().bindTo(registry); + + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(0); + + logger.info("Hello, world!"); + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1); + } + + @Test + void isLevelEnabledDoesntContributeToCounts() { + new io.micrometer.binder.logging.Log4j2Metrics().bindTo(registry); + + Logger logger = LogManager.getLogger(Log4j2MetricsTest.class); + logger.isErrorEnabled(); + + assertThat(registry.get("log4j2.events").tags("level", "error").counter().count()).isEqualTo(0.0); + } + + @Test + void removeFilterFromLoggerContextOnClose() { + new io.micrometer.binder.logging.Log4j2Metrics().bindTo(registry); + + LoggerContext loggerContext = new LoggerContext("test"); + io.micrometer.binder.logging.Log4j2Metrics log4j2Metrics = new io.micrometer.binder.logging.Log4j2Metrics(emptyList(), loggerContext); + log4j2Metrics.bindTo(registry); + + LoggerConfig loggerConfig = loggerContext.getConfiguration().getLoggerConfig(LogManager.ROOT_LOGGER_NAME); + assertThat(loggerConfig.getFilter()).isNotNull(); + log4j2Metrics.close(); + assertThat(loggerConfig.getFilter()).isNull(); + } + + @Test + void noDuplicateLoggingCountWhenMultipleNonAdditiveLoggersShareConfig() { + LoggerContext loggerContext = new LoggerContext("test"); + + LoggerConfig loggerConfig = new LoggerConfig("com.test", Level.INFO, false); + Configuration configuration = loggerContext.getConfiguration(); + configuration.addLogger("com.test", loggerConfig); + loggerContext.setConfiguration(configuration); + loggerContext.updateLoggers(); + + Logger logger1 = loggerContext.getLogger("com.test.log1"); + loggerContext.getLogger("com.test.log2"); + + new io.micrometer.binder.logging.Log4j2Metrics(emptyList(), loggerContext).bindTo(registry); + + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(0); + logger1.info("Hello, world!"); + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(1); + } + + @Issue("#2176") + @Test + void asyncLogShouldNotBeDuplicated() throws IOException { + ConfigurationSource source = new ConfigurationSource(getClass().getResourceAsStream("/binder/logging/log4j2-async-logger.xml")); + Configurator.initialize(null, source); + + Logger logger = LogManager.getLogger(Log4j2MetricsTest.class); + + new Log4j2Metrics().bindTo(registry); + + assertThat(registry.get("log4j2.events").tags("level", "info").counter().count()).isEqualTo(0); + logger.info("Hello, world!"); + await().atMost(Duration.ofSeconds(1)) + .until(() -> registry.get("log4j2.events").tags("level", "info").counter().count() == 1); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/logging/LogbackMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/logging/LogbackMetricsTest.java new file mode 100644 index 0000000000..945989d59f --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/logging/LogbackMetricsTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2017 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.binder.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.cumulative.CumulativeCounter; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.lang.NonNullApi; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +class LogbackMetricsTest { + private MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private Logger logger = (Logger) LoggerFactory.getLogger("foo"); + io.micrometer.binder.logging.LogbackMetrics logbackMetrics; + + @BeforeEach + void bindLogbackMetrics() { + logbackMetrics = new io.micrometer.binder.logging.LogbackMetrics(); + logbackMetrics.bindTo(registry); + } + + @AfterEach + void closeLogbackMetrics() { + if (logbackMetrics != null) { + logbackMetrics.close(); + } + } + + @Test + void logbackLevelMetrics() { + assertThat(registry.get("logback.events").counter().count()).isEqualTo(0.0); + + logger.setLevel(Level.INFO); + + logger.warn("warn"); + logger.error("error"); + logger.debug("debug"); // shouldn't record a metric + + assertThat(registry.get("logback.events").tags("level", "warn").counter().count()).isEqualTo(1.0); + assertThat(registry.get("logback.events").tags("level", "debug").counter().count()).isEqualTo(0.0); + } + + @Issue("#183") + @Test + void isLevelEnabledDoesntContributeToCounts() { + logger.isErrorEnabled(); + + assertThat(registry.get("logback.events").tags("level", "error").counter().count()).isEqualTo(0.0); + } + + @Issue("#411") + @Test + void ignoringLogMetricsInsideCounters() { + registry = new LoggingCounterMeterRegistry(); + try (io.micrometer.binder.logging.LogbackMetrics logbackMetrics = new io.micrometer.binder.logging.LogbackMetrics()) { + logbackMetrics.bindTo(registry); + registry.counter("my.counter").increment(); + } + assertThat(registry.get("logback.events").tags("level", "info").counter().count()).isZero(); + } + + @Issue("#421") + @Test + void removeFilterFromLoggerContextOnClose() { + LoggerContext loggerContext = new LoggerContext(); + + io.micrometer.binder.logging.LogbackMetrics logbackMetrics = new io.micrometer.binder.logging.LogbackMetrics(emptyList(), loggerContext); + logbackMetrics.bindTo(registry); + + assertThat(loggerContext.getTurboFilterList()).hasSize(1); + logbackMetrics.close(); + assertThat(loggerContext.getTurboFilterList()).isEmpty(); + } + + @Issue("#2028") + @Test + void reAddFilterToLoggerContextAfterReset() { + LoggerContext loggerContext = new LoggerContext(); + assertThat(loggerContext.getTurboFilterList()).isEmpty(); + + io.micrometer.binder.logging.LogbackMetrics logbackMetrics = new io.micrometer.binder.logging.LogbackMetrics(emptyList(), loggerContext); + logbackMetrics.bindTo(registry); + + assertThat(loggerContext.getTurboFilterList()).hasSize(1); + loggerContext.reset(); + assertThat(loggerContext.getTurboFilterList()).hasSize(1); + } + + @Issue("#2270") + @Test + void resetIgnoreMetricsWhenRunnableThrows() { + Counter infoLogCounter = registry.get("logback.events").tag("level", "info").counter(); + logger.info("hi"); + assertThat(infoLogCounter.count()).isEqualTo(1); + try { + io.micrometer.binder.logging.LogbackMetrics.ignoreMetrics(() -> { + throw new RuntimeException(); + }); + } catch (RuntimeException ignore) { + } + logger.info("hi"); + assertThat(infoLogCounter.count()).isEqualTo(2); + } + + @NonNullApi + private static class LoggingCounterMeterRegistry extends SimpleMeterRegistry { + @Override + protected Counter newCounter(Meter.Id id) { + return new LoggingCounter(id); + } + } + + private static class LoggingCounter extends CumulativeCounter { + org.slf4j.Logger logger = LoggerFactory.getLogger(LoggingCounter.class); + + LoggingCounter(Id id) { + super(id); + } + + @Override + public void increment() { + LogbackMetrics.ignoreMetrics(() -> { + logger.info("beep"); + super.increment(); + }); + } + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/AbstractMongoDbTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/AbstractMongoDbTest.java new file mode 100644 index 0000000000..657d66b55c --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/AbstractMongoDbTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2019 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.binder.mongodb; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +/** + * Base class for testing MongoDB client based on Testcontainers. + * + * @author Christophe Bornet + * @author Johnny Lim + */ +@Testcontainers +@Tag("docker") +abstract class AbstractMongoDbTest { + + @Container + private final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.0.10")); + + String host; + int port; + + @BeforeEach + void setUp() { + host = mongoDBContainer.getHost(); + port = mongoDBContainer.getFirstMappedPort(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java new file mode 100644 index 0000000000..1c615d7833 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/DefaultMongoCommandTagsProviderTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2021 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.binder.mongodb; + +import java.util.Arrays; + +import com.mongodb.ServerAddress; +import com.mongodb.connection.ClusterId; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.connection.ServerId; +import com.mongodb.event.CommandStartedEvent; +import com.mongodb.event.CommandSucceededEvent; +import io.micrometer.core.instrument.Tag; +import io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider; +import org.bson.BsonBoolean; +import org.bson.BsonDocument; +import org.bson.BsonElement; +import org.bson.BsonString; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit test for {@link io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider}. + * + * @author Chris Bono + */ +class DefaultMongoCommandTagsProviderTest { + + private final ConnectionDescription connectionDesc = new ConnectionDescription( + new ServerId(new ClusterId("cluster1"), new ServerAddress("localhost", 5150))); + + private final io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider tagsProvider = new DefaultMongoCommandTagsProvider(); + + @Test + void defaultCommandTags() { + CommandSucceededEvent event = commandSucceededEvent(5150); + Iterable tags = tagsProvider.commandTags(event); + assertThat(tags).containsExactlyInAnyOrder( + Tag.of("command", "find"), + Tag.of("collection", "unknown"), + Tag.of("cluster.id", connectionDesc.getConnectionId().getServerId().getClusterId().getValue()), + Tag.of("server.address", "localhost:5150"), + Tag.of("status", "SUCCESS") + ); + } + + @Test + void handlesCommandsOverLimitGracefully() { + for (int i = 1; i <= 1000; i++) { + tagsProvider.commandStarted(commandStartedEvent(i)); + } + // At this point we have started 1000 commands (the state map is full at the time) + + // 1001 will not be added to state map and therefore will use 'unknown' + tagsProvider.commandStarted(commandStartedEvent(1001)); + Iterable tags = tagsProvider.commandTags(commandSucceededEvent(1001)); + assertThat(tags).contains(Tag.of("collection", "unknown")); + + // Complete 1000 - which will remove previously added entry from state map + tags = tagsProvider.commandTags(commandSucceededEvent(1000)); + assertThat(tags).contains(Tag.of("collection", "collection-1000")); + + // 1001 will now be put in state map (since 1000 removed and made room for it) + tagsProvider.commandStarted(commandStartedEvent(1001)); + + // 1002 will not be added to state map and therefore will use 'unknown' + tagsProvider.commandStarted(commandStartedEvent(1002)); + + tags = tagsProvider.commandTags(commandSucceededEvent(1001)); + assertThat(tags).contains(Tag.of("collection", "collection-1001")); + + tags = tagsProvider.commandTags(commandSucceededEvent(1002)); + assertThat(tags).contains(Tag.of("collection", "unknown")); + } + + private CommandStartedEvent commandStartedEvent(int requestId) { + return new CommandStartedEvent( + requestId, + connectionDesc, + "db1", + "find", + new BsonDocument("find", new BsonString("collection-" + requestId))); + } + + private CommandSucceededEvent commandSucceededEvent(int requestId) { + return new CommandSucceededEvent( + requestId, + connectionDesc, + "find", + new BsonDocument(), + 1200L); + } + + @Nested + class DetermineCollectionName { + + @Test + void withNameInAllowList() { + assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", new BsonString(" bar ")))).hasValue("bar"); + } + + @Test + void withNameNotInAllowList() { + assertThat(tagsProvider.determineCollectionName("cmd", new BsonDocument("cmd", new BsonString(" bar ")))).isEmpty(); + } + + @Test + void withNameNotInCommand() { + assertThat(tagsProvider.determineCollectionName("find", new BsonDocument())).isEmpty(); + } + + @Test + void withNonStringCommand() { + assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", BsonBoolean.TRUE))).isEmpty(); + } + + @Test + void withEmptyStringCommand() { + assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("find", new BsonString(" ")))).isEmpty(); + } + + @Test + void withCollectionFieldOnly() { + assertThat(tagsProvider.determineCollectionName("find", new BsonDocument("collection", new BsonString(" bar ")))).hasValue("bar"); + } + + @Test + void withCollectionFieldAndAllowListedCommand() { + BsonDocument command = new BsonDocument(Arrays.asList( + new BsonElement("collection", new BsonString("coll")), + new BsonElement("find", new BsonString("bar")) + )); + assertThat(tagsProvider.determineCollectionName("find", command)).hasValue("bar"); + } + + @Test + void withCollectionFieldAndNotAllowListedCommand() { + BsonDocument command = new BsonDocument(Arrays.asList( + new BsonElement("collection", new BsonString("coll")), + new BsonElement("cmd", new BsonString("bar")) + )); + assertThat(tagsProvider.determineCollectionName("find", command)).hasValue("coll"); + } + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsCommandListenerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsCommandListenerTest.java new file mode 100644 index 0000000000..0a4265ebe1 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsCommandListenerTest.java @@ -0,0 +1,236 @@ +/* + * Copyright 2019 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.binder.mongodb; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.event.ClusterListener; +import com.mongodb.event.ClusterOpeningEvent; +import com.mongodb.event.CommandEvent; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider; +import io.micrometer.binder.mongodb.MongoCommandTagsProvider; +import io.micrometer.binder.mongodb.MongoMetricsCommandListener; +import io.micrometer.core.instrument.search.MeterNotFoundException; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link io.micrometer.binder.mongodb.MongoMetricsCommandListener}. + * + * @author Christophe Bornet + * @author Chris Bono + */ +class MongoMetricsCommandListenerTest extends AbstractMongoDbTest { + + private MeterRegistry registry; + + private AtomicReference clusterId; + + private MongoClient mongo; + + @BeforeEach + void setup() { + registry = new SimpleMeterRegistry(); + clusterId = new AtomicReference<>(); + MongoClientSettings settings = MongoClientSettings.builder() + .addCommandListener(new io.micrometer.binder.mongodb.MongoMetricsCommandListener(registry)) + .applyToClusterSettings(builder -> builder + .hosts(singletonList(new ServerAddress(host, port))) + .addClusterListener(new ClusterListener() { + @Override + public void clusterOpening(ClusterOpeningEvent event) { + clusterId.set(event.getClusterId().getValue()); + } + })).build(); + mongo = MongoClients.create(settings); + } + + @Test + void shouldCreateSuccessCommandMetric() { + mongo.getDatabase("test") + .getCollection("testCol") + .insertOne(new Document("testDoc", new Date())); + + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port), + "command", "insert", + "collection", "testCol", + "status", "SUCCESS" + ); + assertThat(registry.get("mongodb.driver.commands").tags(tags).timer().count()).isEqualTo(1); + } + + @Test + void shouldCreateFailedCommandMetric() { + mongo.getDatabase("test") + .getCollection("testCol") + .dropIndex("nonExistentIndex"); + + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port), + "command", "dropIndexes", + "collection", "testCol", + "status", "FAILED" + ); + assertThat(registry.get("mongodb.driver.commands").tags(tags).timer().count()).isEqualTo(1); + } + + @Test + void shouldCreateSuccessCommandMetricWithCustomSettings() { + io.micrometer.binder.mongodb.MongoCommandTagsProvider tagsProvider = new io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider() { + @Override + public Iterable commandTags(CommandEvent event) { + return Tags.of(super.commandTags(event)).and(Tag.of("mongoz", "5150")); + } + }; + MongoClientSettings settings = MongoClientSettings.builder() + .addCommandListener(new io.micrometer.binder.mongodb.MongoMetricsCommandListener(registry, tagsProvider)) + .applyToClusterSettings(builder -> builder + .hosts(singletonList(new ServerAddress(host, port))) + .addClusterListener(new ClusterListener() { + @Override + public void clusterOpening(ClusterOpeningEvent event) { + clusterId.set(event.getClusterId().getValue()); + } + })) + .build(); + try (MongoClient mongo = MongoClients.create(settings)) { + mongo.getDatabase("test") + .getCollection("testCol") + .insertOne(new Document("testDoc", new Date())); + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port), + "command", "insert", + "collection", "testCol", + "status", "SUCCESS", + "mongoz", "5150" + ); + assertThat(registry.get("mongodb.driver.commands").tags(tags).timer().count()).isEqualTo(1); + } + } + + @Test + void shouldCreateFailedCommandMetricWithCustomSettings() { + MongoCommandTagsProvider tagsProvider = new DefaultMongoCommandTagsProvider() { + @Override + public Iterable commandTags(CommandEvent event) { + return Tags.of(super.commandTags(event)).and(Tag.of("mongoz", "5150")); + } + }; + MongoClientSettings settings = MongoClientSettings.builder() + .addCommandListener(new MongoMetricsCommandListener(registry, tagsProvider)) + .applyToClusterSettings(builder -> builder + .hosts(singletonList(new ServerAddress(host, port))) + .addClusterListener(new ClusterListener() { + @Override + public void clusterOpening(ClusterOpeningEvent event) { + clusterId.set(event.getClusterId().getValue()); + } + })) + .build(); + try (MongoClient mongo = MongoClients.create(settings)) { + mongo.getDatabase("test") + .getCollection("testCol") + .dropIndex("nonExistentIndex"); + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port), + "command", "dropIndexes", + "collection", "testCol", + "status", "FAILED", + "mongoz", "5150" + ); + assertThat(registry.get("mongodb.driver.commands").tags(tags).timer().count()).isEqualTo(1); + } + } + + @Test + void shouldSupportConcurrentCommands() throws InterruptedException { + for (int i = 0; i < 20; i++) { + Map commandThreadMap = new HashMap<>(); + + commandThreadMap.put("insert", new Thread(() -> mongo.getDatabase("test") + .getCollection("testCol") + .insertOne(new Document("testField", new Date())))); + + commandThreadMap.put("update", new Thread(() -> mongo.getDatabase("test") + .getCollection("testCol") + .updateOne(new Document("nonExistentField", "abc"), + new Document("$set", new Document("nonExistentField", "xyz"))))); + + commandThreadMap.put("delete", new Thread(() -> mongo.getDatabase("test") + .getCollection("testCol") + .deleteOne(new Document("nonExistentField", "abc")))); + + commandThreadMap.put("aggregate", new Thread(() -> mongo.getDatabase("test") + .getCollection("testCol") + .countDocuments())); + + for (Thread thread : commandThreadMap.values()) { + thread.start(); + } + + for (Thread thread : commandThreadMap.values()) { + thread.join(); + } + + final int iterationsCompleted = i + 1; + + for (String command : commandThreadMap.keySet()) { + long commandsRecorded; + try { + commandsRecorded = registry.get("mongodb.driver.commands") + .tags(Tags.of("command", command)) + .timer() + .count(); + } catch (MeterNotFoundException e) { + commandsRecorded = 0L; + } + + assertThat(commandsRecorded) + .as("Check how many %s commands were recorded after %d iterations", command, iterationsCompleted) + .isEqualTo(iterationsCompleted); + } + } + } + + @AfterEach + void destroy() { + if (mongo != null) { + mongo.close(); + } + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListenerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListenerTest.java new file mode 100644 index 0000000000..ee12615612 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/mongodb/MongoMetricsConnectionPoolListenerTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019 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.binder.mongodb; + +import java.util.concurrent.atomic.AtomicReference; + +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.connection.ClusterId; +import com.mongodb.connection.ConnectionId; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.connection.ServerId; +import com.mongodb.event.ClusterListener; +import com.mongodb.event.ClusterOpeningEvent; +import com.mongodb.event.ConnectionCheckedInEvent; +import com.mongodb.event.ConnectionCheckedOutEvent; +import com.mongodb.event.ConnectionPoolClosedEvent; +import com.mongodb.event.ConnectionPoolCreatedEvent; +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Tests for {@link io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener}. + * + * @author Christophe Bornet + * @author Jonatan Ivanov + */ +class MongoMetricsConnectionPoolListenerTest extends AbstractMongoDbTest { + + private final MeterRegistry registry = new SimpleMeterRegistry(); + + @Test + void shouldCreatePoolMetrics() { + AtomicReference clusterId = new AtomicReference<>(); + MongoClientSettings settings = MongoClientSettings.builder() + .applyToConnectionPoolSettings(builder -> builder + .minSize(2) + .addConnectionPoolListener(new io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener(registry)) + ) + .applyToClusterSettings(builder -> builder + .hosts(singletonList(new ServerAddress(host, port))) + .addClusterListener(new ClusterListener() { + @Override + public void clusterOpening(ClusterOpeningEvent event) { + clusterId.set(event.getClusterId().getValue()); + } + })) + .build(); + MongoClient mongo = MongoClients.create(settings); + + mongo.getDatabase("test") + .createCollection("testCol"); + + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port) + ); + + assertThat(registry.get("mongodb.driver.pool.size").tags(tags).gauge().value()).isEqualTo(2); + assertThat(registry.get("mongodb.driver.pool.checkedout").gauge().value()).isZero(); + assertThat(registry.get("mongodb.driver.pool.waitqueuesize").gauge().value()).isZero(); + + mongo.close(); + + assertThat(registry.find("mongodb.driver.pool.size").tags(tags).gauge()) + .describedAs("metrics should be removed when the connection pool is closed") + .isNull(); + } + + @Test + void shouldCreatePoolMetricsWithCustomTags() { + MeterRegistry registry = new SimpleMeterRegistry(); + AtomicReference clusterId = new AtomicReference<>(); + io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener connectionPoolListener = new io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener(registry, e -> + Tags.of( + "cluster.id", e.getServerId().getClusterId().getValue(), + "server.address", e.getServerId().getAddress().toString(), + "my.custom.connection.pool.identifier", "custom" + )); + MongoClientSettings settings = MongoClientSettings.builder() + .applyToConnectionPoolSettings(builder -> builder + .minSize(2) + .addConnectionPoolListener(connectionPoolListener) + ) + .applyToClusterSettings(builder -> builder + .hosts(singletonList(new ServerAddress(host, port))) + .addClusterListener(new ClusterListener() { + @Override + public void clusterOpening(ClusterOpeningEvent event) { + clusterId.set(event.getClusterId().getValue()); + } + })) + .build(); + MongoClient mongo = MongoClients.create(settings); + + mongo.getDatabase("test") + .createCollection("testCol"); + + Tags tags = Tags.of( + "cluster.id", clusterId.get(), + "server.address", String.format("%s:%s", host, port), + "my.custom.connection.pool.identifier", "custom" + ); + + assertThat(registry.get("mongodb.driver.pool.size").tags(tags).gauge().value()).isEqualTo(2); + assertThat(registry.get("mongodb.driver.pool.checkedout").gauge().value()).isZero(); + assertThat(registry.get("mongodb.driver.pool.waitqueuesize").gauge().value()).isZero(); + + mongo.close(); + + assertThat(registry.find("mongodb.driver.pool.size").tags(tags).gauge()) + .describedAs("metrics should be removed when the connection pool is closed") + .isNull(); + } + + @Issue("#2384") + void whenConnectionCheckedInAfterPoolClose_thenNoExceptionThrown() { + ServerId serverId = new ServerId(new ClusterId(), new ServerAddress(host, port)); + ConnectionId connectionId = new ConnectionId(serverId); + io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener listener = new MongoMetricsConnectionPoolListener(registry); + listener.connectionPoolCreated(new ConnectionPoolCreatedEvent(serverId, ConnectionPoolSettings.builder().build())); + listener.connectionCheckedOut(new ConnectionCheckedOutEvent(connectionId)); + listener.connectionPoolClosed(new ConnectionPoolClosedEvent(serverId)); + assertThatCode(() -> listener.connectionCheckedIn(new ConnectionCheckedInEvent(connectionId))) + .doesNotThrowAnyException(); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetricsTest.java new file mode 100644 index 0000000000..c40ad4075a --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpConnectionPoolMetricsTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 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.binder.okhttp3; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics; +import io.micrometer.core.instrument.search.MeterNotFoundException; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import okhttp3.ConnectionPool; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Ben Hubert + */ +class OkHttpConnectionPoolMetricsTest { + private final MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + private final ConnectionPool connectionPool = mock(ConnectionPool.class); + + @Test + void creationWithNullConnectionPoolThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(null)); + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(null, Tags.empty())); + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(null, "irrelevant", Tags.empty())); + } + + @Test + void creationWithNullNamePrefixThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, null, Tags.empty())); + } + + @Test + void creationWithNullTagsThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, null)); + assertThrows(IllegalArgumentException.class, () -> new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, "irrelevant.name", null)); + } + + @Test + void instanceUsesDefaultNamePrefix() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool); + instance.bindTo(registry); + registry.get("okhttp.pool.connection.count"); // does not throw MeterNotFoundException + } + + @Test + void instanceUsesDefaultNamePrefixAndGivenTag() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, Tags.of("foo", "bar")); + instance.bindTo(registry); + registry.get("okhttp.pool.connection.count").tags("foo", "bar"); // does not throw MeterNotFoundException + } + + @Test + void instanceUsesGivenNamePrefix() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, "some.meter", Tags.empty()); + instance.bindTo(registry); + registry.get("some.meter.connection.count"); // does not throw MeterNotFoundException + } + + @Test + void instanceUsesGivenNamePrefixAndTag() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, "another.meter", Tags.of("bar", "baz")); + instance.bindTo(registry); + registry.get("another.meter.connection.count").tags("bar", "baz"); // does not throw MeterNotFoundException + } + + @Test + void activeAndIdle() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, Tags.of("foo", "bar")); + instance.bindTo(registry); + when(connectionPool.connectionCount()).thenReturn(17); + when(connectionPool.idleConnectionCount()).thenReturn(10, 9); + + assertThat(registry.get("okhttp.pool.connection.count") + .tags(Tags.of("foo", "bar", "state", "active")) + .gauge().value()).isEqualTo(7.0); + assertThat(registry.get("okhttp.pool.connection.count") + .tags(Tags.of("foo", "bar", "state", "idle")) + .gauge().value()).isEqualTo(10.0); + + assertThat(registry.get("okhttp.pool.connection.count") + .tags(Tags.of("foo", "bar", "state", "active")) + .gauge().value()).isEqualTo(8.0); + assertThat(registry.get("okhttp.pool.connection.count") + .tags(Tags.of("foo", "bar", "state", "idle")) + .gauge().value()).isEqualTo(9.0); + } + + @Test + void maxIfGiven() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics(connectionPool, "huge.pool", Tags.of("foo", "bar"), 1234); + instance.bindTo(registry); + assertThat(registry.get("huge.pool.connection.limit") + .tags(Tags.of("foo", "bar")) + .gauge().value()).isEqualTo(1234.0); + } + + @Test + void maxIfNotGiven() { + io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics instance = new OkHttpConnectionPoolMetrics(connectionPool, "huge.pool", Tags.of("foo", "bar"), null); + instance.bindTo(registry); + assertThrows(MeterNotFoundException.class, () -> registry.get("huge.pool.connection.limit") + .tags(Tags.of("foo", "bar")) + .gauge()); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListenerTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListenerTest.java new file mode 100644 index 0000000000..58ea96bcff --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/okhttp3/OkHttpMetricsEventListenerTest.java @@ -0,0 +1,310 @@ +/* + * Copyright 2017 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.binder.okhttp3; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import com.github.tomakehurst.wiremock.WireMockServer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.okhttp3.OkHttpMetricsEventListener; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import okhttp3.Cache; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import ru.lanwen.wiremock.ext.WiremockResolver; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.any; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link io.micrometer.binder.okhttp3.OkHttpMetricsEventListener}. + * + * @author Bjarte S. Karlsen + * @author Jon Schneider + * @author Johnny Lim + * @author Nurettin Yilmaz + */ +@ExtendWith(WiremockResolver.class) +class OkHttpMetricsEventListenerTest { + + private static final String URI_EXAMPLE_VALUE = "uriExample"; + private static final Function URI_MAPPER = req -> URI_EXAMPLE_VALUE; + + private MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + + private OkHttpClient client = new OkHttpClient.Builder() + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .tags(Tags.of("foo", "bar")) + .uriMapper(URI_MAPPER) + .build()) + .build(); + + @Test + void timeSuccessful(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + Request request = new Request.Builder() + .url(server.baseUrl()) + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "status", "200", "uri", URI_EXAMPLE_VALUE, + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().count()).isEqualTo(1L); + } + + @Test + void timeNotFound(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(404))); + Request request = new Request.Builder() + .url(server.baseUrl()) + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "status", "404", "uri", "NOT_FOUND", + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().count()).isEqualTo(1L); + } + + @Test + void timeFailureDueToTimeout(@WiremockResolver.Wiremock WireMockServer server) { + Request request = new Request.Builder() + .url(server.baseUrl()) + .build(); + + server.stop(); + + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(1, TimeUnit.MILLISECONDS) + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .tags(Tags.of("foo", "bar")) + .uriMapper(URI_MAPPER) + .build()) + .build(); + + try { + client.newCall(request).execute().close(); + fail("Expected IOException."); + } catch (IOException ignored) { + // expected + } + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "uri", URI_EXAMPLE_VALUE, "status", "IO_ERROR", "target.host", "localhost") + .timer().count()).isEqualTo(1L); + } + + @Test + void uriTagWorksWithUriPatternHeader(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + Request request = new Request.Builder() + .url(server.baseUrl() + "/helloworld.txt") + .header(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.URI_PATTERN, "/") + .build(); + + client = new OkHttpClient.Builder() + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .tags(Tags.of("foo", "bar")) + .build()) + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "uri", "/", "status", "200", + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().count()).isEqualTo(1L); + } + + @Test + void uriTagWorksWithUriMapper(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + OkHttpClient client = new OkHttpClient.Builder() + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .uriMapper(req -> req.url().encodedPath()) + .tags(Tags.of("foo", "bar")) + .build()) + .build(); + + Request request = new Request.Builder() + .url(server.baseUrl() + "/helloworld.txt") + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "uri", "/helloworld.txt", "status", "200", + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().count()).isEqualTo(1L); + } + + @Test + void contextSpecificTags(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + OkHttpClient client = new OkHttpClient.Builder() + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .tag((req, res) -> Tag.of("another.uri", req.url().encodedPath())) + .build()) + .build(); + + Request request = new Request.Builder() + .url(server.baseUrl() + "/helloworld.txt") + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("another.uri", "/helloworld.txt", "status", "200") + .timer().count()).isEqualTo(1L); + } + + @Test + void cachedResponsesDoNotLeakMemory( + @WiremockResolver.Wiremock WireMockServer server, @TempDir Path tempDir) throws IOException { + io.micrometer.binder.okhttp3.OkHttpMetricsEventListener listener = io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests").build(); + OkHttpClient clientWithCache = new OkHttpClient.Builder() + .eventListener(listener) + .cache(new Cache(tempDir.toFile(), 55555)) + .build(); + server.stubFor(any(anyUrl()).willReturn(aResponse().withHeader("Cache-Control", "max-age=9600"))); + Request request = new Request.Builder() + .url(server.baseUrl()) + .build(); + + clientWithCache.newCall(request).execute().close(); + assertThat(listener.callState).isEmpty(); + try (Response response = clientWithCache.newCall(request).execute()) { + assertThat(response.cacheResponse()).isNotNull(); + } + + assertThat(listener.callState).isEmpty(); + } + + @Test + void requestTagsWithClass(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + Request request = new Request.Builder() + .url(server.baseUrl() + "/helloworld.txt") + .tag(Tags.class, Tags.of("requestTag1", "tagValue1")) + .build(); + + testRequestTags(server, request); + } + + @Test + void requestTagsWithoutClass(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + Request request = new Request.Builder() + .url(server.baseUrl() + "/helloworld.txt") + .tag(Tags.of("requestTag1", "tagValue1")) + .build(); + + testRequestTags(server, request); + } + + @Test + void hostTagCanBeDisabled(@WiremockResolver.Wiremock WireMockServer server) throws IOException { + server.stubFor(any(anyUrl())); + OkHttpClient client = new OkHttpClient.Builder() + .eventListener(io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .includeHostTag(false) + .build()) + .build(); + Request request = new Request.Builder() + .url(server.baseUrl()) + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("status", "200", + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().getId().getTags()).doesNotContain(Tag.of("host", "localhost")); + } + + @Test + void timeWhenRequestIsNull() { + io.micrometer.binder.okhttp3.OkHttpMetricsEventListener listener = io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests").build(); + io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.CallState state = new io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.CallState(registry.config().clock().monotonicTime(), null); + listener.time(state); + + assertThat(registry.get("okhttp.requests") + .tags("uri", "UNKNOWN", + "target.host", "UNKNOWN", + "target.port", "UNKNOWN", + "target.scheme", "UNKNOWN") + .timer().count()).isEqualTo(1L); + } + + @Test + void timeWhenRequestIsNullAndRequestTagKeysAreGiven() { + io.micrometer.binder.okhttp3.OkHttpMetricsEventListener listener = io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .requestTagKeys("tag1", "tag2").build(); + io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.CallState state = new io.micrometer.binder.okhttp3.OkHttpMetricsEventListener.CallState(registry.config().clock().monotonicTime(), null); + listener.time(state); + + assertThat(registry.get("okhttp.requests") + .tags("uri", "UNKNOWN", + "tag1", "UNKNOWN", + "tag2", "UNKNOWN") + .timer().count()).isEqualTo(1L); + } + + private void testRequestTags(@WiremockResolver.Wiremock WireMockServer server, Request request) throws IOException { + server.stubFor(any(anyUrl())); + OkHttpClient client = new OkHttpClient.Builder() + .eventListener(OkHttpMetricsEventListener.builder(registry, "okhttp.requests") + .uriMapper(req -> req.url().encodedPath()) + .tags(Tags.of("foo", "bar")) + .build()) + .build(); + + client.newCall(request).execute().close(); + + assertThat(registry.get("okhttp.requests") + .tags("foo", "bar", "uri", "/helloworld.txt", "status", "200", "requestTag1", "tagValue1", + "target.host", "localhost", + "target.port", String.valueOf(server.port()), + "target.scheme", "http") + .timer().count()).isEqualTo(1L); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/system/DiskSpaceMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/system/DiskSpaceMetricsTest.java new file mode 100644 index 0000000000..09f2b2a5bf --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/system/DiskSpaceMetricsTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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.binder.system; + +import java.io.File; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.system.DiskSpaceMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Tests for {@link io.micrometer.binder.system.DiskSpaceMetrics}. + * + * @author jmcshane + * @author Johnny Lim + * @author Tommy Ludwig + */ +class DiskSpaceMetricsTest { + + MeterRegistry registry = new SimpleMeterRegistry(); + + @Test + void diskSpaceMetrics() { + new io.micrometer.binder.system.DiskSpaceMetrics(new File(System.getProperty("user.dir"))).bindTo(registry); + + assertThat(registry.get("disk.free").gauge().value()).isNotNaN().isGreaterThan(0); + assertThat(registry.get("disk.total").gauge().value()).isNotNaN().isGreaterThan(0); + } + + @Test + void diskSpaceMetricsWithTags() { + new io.micrometer.binder.system.DiskSpaceMetrics(new File(System.getProperty("user.dir")), Tags.of("key1", "value1")).bindTo(registry); + + assertThat(registry.get("disk.free").tags("key1", "value1").gauge().value()).isNotNaN().isGreaterThan(0); + assertThat(registry.get("disk.total").tags("key1", "value1").gauge().value()).isNotNaN().isGreaterThan(0); + } + + @Test + void garbageCollectionDoesNotLoseGaugeValue() { + new DiskSpaceMetrics(new File(System.getProperty("user.dir"))).bindTo(registry); + + System.gc(); + + assertThat(registry.get("disk.free").gauge().value()).isNotNaN().isGreaterThan(0); + assertThat(registry.get("disk.total").gauge().value()).isNotNaN().isGreaterThan(0); + } + +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/system/FileDescriptorMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/system/FileDescriptorMetricsTest.java new file mode 100644 index 0000000000..4143bebff6 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/system/FileDescriptorMetricsTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 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.binder.system; + +import java.lang.management.OperatingSystemMXBean; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.binder.system.FileDescriptorMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link io.micrometer.binder.system.FileDescriptorMetrics} + * + * @author Michael Weirauch + */ +class FileDescriptorMetricsTest { + private MeterRegistry registry = new SimpleMeterRegistry(); + + @Test + void fileDescriptorMetricsUnsupportedOsBeanMock() { + final OperatingSystemMXBean osBean = mock(UnsupportedOperatingSystemMXBean.class); + new io.micrometer.binder.system.FileDescriptorMetrics(osBean, Tags.of("some", "tag")).bindTo(registry); + + assertThat(registry.find("process.files.open").gauge()).isNull(); + assertThat(registry.find("process.files.max").gauge()).isNull(); + } + + @Test + void unixFileDescriptorMetrics() { + assumeFalse(System.getProperty("os.name").toLowerCase().contains("win")); + + new io.micrometer.binder.system.FileDescriptorMetrics(Tags.of("some", "tag")).bindTo(registry); + + assertThat(registry.get("process.files.open").tags("some", "tag") + .gauge().value()).isGreaterThan(0); + assertThat(registry.get("process.files.max").tags("some", "tag") + .gauge().value()).isGreaterThan(0); + } + + @Test + void windowsFileDescriptorMetrics() { + assumeTrue(System.getProperty("os.name").toLowerCase().contains("win")); + + new FileDescriptorMetrics(Tags.of("some", "tag")).bindTo(registry); + + assertThat(registry.find("process.files.open").gauge()).isNull(); + assertThat(registry.find("process.files.max").gauge()).isNull(); + } + + /** Represents a JVM implementation we do not currently support. */ + private interface UnsupportedOperatingSystemMXBean extends OperatingSystemMXBean { + long getOpenFileDescriptorCount(); + long getMaxFileDescriptorCount(); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/system/ProcessorMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/system/ProcessorMetricsTest.java new file mode 100644 index 0000000000..258fa47c14 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/system/ProcessorMetricsTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 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.binder.system; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.binder.system.ProcessorMetrics; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Tests for {@link io.micrometer.binder.system.ProcessorMetrics}. + * + * @author Jon Schneider + * @author Michael Weirauch + * @author Clint Checketts + * @author Tommy Ludwig + * @author Johnny Lim + */ +class ProcessorMetricsTest { + + MeterRegistry registry = new SimpleMeterRegistry(); + + @BeforeEach + void setup() { + new ProcessorMetrics().bindTo(registry); + } + + @Test + void cpuMetrics() { + assertThat(registry.get("system.cpu.count").gauge().value()).isGreaterThan(0); + if (System.getProperty("os.name").toLowerCase().contains("win")) { + assertThat(registry.find("system.load.average.1m").gauge()) + .describedAs("Not present on windows").isNull(); + } else { + assertThat(registry.get("system.load.average.1m").gauge().value()).isGreaterThanOrEqualTo(0); + } + } + + @Test + void hotspotCpuMetrics() { + assumeTrue(!isOpenJ9()); + + assertThat(registry.get("system.cpu.usage").gauge().value()).isGreaterThanOrEqualTo(0); + assertThat(registry.get("process.cpu.usage").gauge().value()).isGreaterThanOrEqualTo(0); + } + + @Test + void openJ9CpuMetrics() { + assumeTrue(isOpenJ9()); + + /* + * These methods are documented to return "-1" on the first call + * and a positive value - if supported - on subsequent calls. + * This holds true for "system.cpu.usage" but not for "process.cpu.usage". The latter + * needs some milliseconds of sleep before it actually returns a positive value + * on a supported system. Thread.sleep() is flaky, though. + */ + assertThat(registry.get("system.cpu.usage").gauge().value()).isGreaterThanOrEqualTo(-1); + assertThat(registry.get("process.cpu.usage").gauge().value()).isGreaterThanOrEqualTo(-1); + } + + private boolean isOpenJ9() { + return classExists("com.ibm.lang.management.OperatingSystemMXBean"); + } + + private boolean classExists(String className) { + try { + Class.forName(className); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/system/UptimeMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/system/UptimeMetricsTest.java new file mode 100644 index 0000000000..82446d5e5a --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/system/UptimeMetricsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 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.binder.system; + +import java.lang.management.RuntimeMXBean; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.binder.system.UptimeMetrics; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Uptime metrics. + * + * @author Michael Weirauch + */ +class UptimeMetricsTest { + + @Test + void uptimeMetricsRuntime() { + MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + new io.micrometer.binder.system.UptimeMetrics().bindTo(registry); + + registry.get("process.uptime").timeGauge(); + registry.get("process.start.time").timeGauge(); + } + + @Test + void uptimeMetricsMock() { + MeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + RuntimeMXBean runtimeMXBean = mock(RuntimeMXBean.class); + when(runtimeMXBean.getUptime()).thenReturn(1337L); + when(runtimeMXBean.getStartTime()).thenReturn(4711L); + new UptimeMetrics(runtimeMXBean, emptyList()).bindTo(registry); + + assertThat(registry.get("process.uptime").timeGauge().value()).isEqualTo(1.337); + assertThat(registry.get("process.start.time").timeGauge().value()).isEqualTo(4.711); + } +} diff --git a/micrometer-binders/src/test/java/io/micrometer/binder/tomcat/TomcatMetricsTest.java b/micrometer-binders/src/test/java/io/micrometer/binder/tomcat/TomcatMetricsTest.java new file mode 100644 index 0000000000..4564d52e53 --- /dev/null +++ b/micrometer-binders/src/test/java/io/micrometer/binder/tomcat/TomcatMetricsTest.java @@ -0,0 +1,374 @@ +/* + * Copyright 2017 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.binder.tomcat; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.servlet.Servlet; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import io.micrometer.binder.Issue; +import io.micrometer.core.instrument.FunctionTimer; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.instrument.util.IOUtils; +import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.session.ManagerBase; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.session.TooManyActiveSessionsException; +import org.apache.catalina.startup.Tomcat; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + + +/** + * Tests for {@link io.micrometer.binder.tomcat.TomcatMetrics}. + * + * @author Clint Checketts + * @author Jon Schneider + * @author Johnny Lim + */ +class TomcatMetricsTest { + private static final int PROCESSING_TIME_IN_MILLIS = 10; + + private SimpleMeterRegistry registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + + private int port; + + @Test + void managerBasedMetrics() { + Context context = new StandardContext(); + + ManagerBase manager = new ManagerBase() { + @Override + public void load() { + } + + @Override + public void unload() { + } + + @Override + public Context getContext() { + return context; + } + }; + + manager.setMaxActiveSessions(3); + + manager.createSession("first"); + manager.createSession("second"); + manager.createSession("third"); + + try { + manager.createSession("fourth"); + fail("TooManyActiveSessionsException expected."); + } catch (TooManyActiveSessionsException exception) { + //ignore error, testing rejection + } + + StandardSession expiredSession = new StandardSession(manager); + expiredSession.setId("third"); + expiredSession.setCreationTime(System.currentTimeMillis() - 10_000); + manager.remove(expiredSession, true); + + Iterable tags = Tags.of("metricTag", "val1"); + io.micrometer.binder.tomcat.TomcatMetrics.monitor(registry, manager, tags); + + assertThat(registry.get("tomcat.sessions.active.max").tags(tags).gauge().value()).isEqualTo(3.0); + assertThat(registry.get("tomcat.sessions.active.current").tags(tags).gauge().value()).isEqualTo(2.0); + assertThat(registry.get("tomcat.sessions.expired").tags(tags).functionCounter().count()).isEqualTo(1.0); + assertThat(registry.get("tomcat.sessions.rejected").tags(tags).functionCounter().count()).isEqualTo(1.0); + assertThat(registry.get("tomcat.sessions.created").tags(tags).functionCounter().count()).isEqualTo(3.0); + assertThat(registry.get("tomcat.sessions.alive.max").tags(tags).timeGauge().value()).isGreaterThan(1.0); + } + + private void sleep() { + try { + Thread.sleep(PROCESSING_TIME_IN_MILLIS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + + @Test + void whenTomcatMetricsBoundBeforeTomcatStarted_mbeanMetricsRegisteredEventually() throws Exception { + io.micrometer.binder.tomcat.TomcatMetrics.monitor(registry, null); + + CountDownLatch latch = new CountDownLatch(1); + registry.config().onMeterAdded(m -> { + if (m.getId().getName().equals("tomcat.global.received")) + latch.countDown(); + }); + + HttpServlet servlet = new HttpServlet() { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("yes".getBytes()); + } + }; + + runTomcat(servlet, () -> { + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + + checkMbeansInitialState(); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost post = new HttpPost("http://localhost:" + this.port + "/0"); + post.setEntity(new StringEntity("you there?")); + CloseableHttpResponse response1 = httpClient.execute(post); + + CloseableHttpResponse response2 = httpClient.execute( + new HttpGet("http://localhost:" + this.port + "/0/no-get")); + + long expectedSentBytes = response1.getEntity().getContentLength() + + response2.getEntity().getContentLength(); + checkMbeansAfterRequests(expectedSentBytes); + } + + return null; + }); + } + + @Test + void whenTomcatMetricsBoundAfterTomcatStarted_mbeanMetricsRegisteredImmediately() throws Exception { + HttpServlet servlet = new HttpServlet() { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("yes".getBytes()); + } + }; + + runTomcat(servlet, () -> { + io.micrometer.binder.tomcat.TomcatMetrics.monitor(registry, null); + + checkMbeansInitialState(); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost post = new HttpPost("http://localhost:" + this.port + "/0"); + post.setEntity(new StringEntity("you there?")); + CloseableHttpResponse response1 = httpClient.execute(post); + + CloseableHttpResponse response2 = httpClient.execute( + new HttpGet("http://localhost:" + this.port + "/0/no-get")); + + long expectedSentBytes = response1.getEntity().getContentLength() + + response2.getEntity().getContentLength(); + checkMbeansAfterRequests(expectedSentBytes); + } + + return null; + }); + } + + @Test + @Issue("#1989") + void whenMultipleServlets_thenRegisterMetricsForAllServlets() throws Exception { + Collection servlets = Arrays.asList(new HttpServlet() { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("yes".getBytes()); + } + }, new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("hi".getBytes()); + } + }); + + runTomcat(servlets, () -> { + io.micrometer.binder.tomcat.TomcatMetrics.monitor(registry, null); + + checkMbeansInitialState(); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost post = new HttpPost("http://localhost:" + this.port + "/0"); + post.setEntity(new StringEntity("you there?")); + CloseableHttpResponse response1 = httpClient.execute(post); + + CloseableHttpResponse response2 = httpClient.execute( + new HttpGet("http://localhost:" + this.port + "/1")); + + FunctionTimer servlet0 = registry.get("tomcat.servlet.request").tag("name", "servlet0").functionTimer(); + FunctionTimer servlet1 = registry.get("tomcat.servlet.request").tag("name", "servlet1").functionTimer(); + assertThat(servlet0.count()).isEqualTo(1); + assertThat(servlet0.totalTime(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + assertThat(servlet1.count()).isEqualTo(1); + assertThat(servlet1.totalTime(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + } + + return null; + }); + } + + @Test + @Issue("#1989") + void whenMultipleServletsAndTomcatMetricsBoundBeforeTomcatStarted_thenEventuallyRegisterMetricsForAllServlets() throws Exception { + TomcatMetrics.monitor(registry, null); + CountDownLatch latch0 = new CountDownLatch(1); + CountDownLatch latch1 = new CountDownLatch(1); + registry.config().onMeterAdded(m -> { + if (m.getId().getName().equals("tomcat.servlet.error")) { + if ("servlet0".equals(m.getId().getTag("name"))) { + latch0.countDown(); + } else if ("servlet1".equals(m.getId().getTag("name"))) { + latch1.countDown(); + } + } + }); + + Collection servlets = Arrays.asList(new HttpServlet() { + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("yes".getBytes()); + } + }, new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + IOUtils.toString(req.getInputStream()); + sleep(); + resp.getOutputStream().write("hi".getBytes()); + } + }); + + runTomcat(servlets, () -> { + assertThat(latch0.await(3, TimeUnit.SECONDS)).isTrue(); + assertThat(latch1.await(3, TimeUnit.SECONDS)).isTrue(); + + checkMbeansInitialState(); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost post = new HttpPost("http://localhost:" + this.port + "/0"); + post.setEntity(new StringEntity("you there?")); + CloseableHttpResponse response1 = httpClient.execute(post); + + CloseableHttpResponse response2 = httpClient.execute( + new HttpGet("http://localhost:" + this.port + "/1")); + + FunctionTimer servlet0 = registry.get("tomcat.servlet.request").tag("name", "servlet0").functionTimer(); + FunctionTimer servlet1 = registry.get("tomcat.servlet.request").tag("name", "servlet1").functionTimer(); + assertThat(servlet0.count()).isEqualTo(1); + assertThat(servlet0.totalTime(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + assertThat(servlet1.count()).isEqualTo(1); + assertThat(servlet1.totalTime(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + } + + return null; + }); + } + + void runTomcat(HttpServlet servlet, Callable doWithTomcat) throws Exception { + runTomcat(Collections.singleton(servlet), doWithTomcat); + } + + void runTomcat(Collection servlets, Callable doWithTomcat) throws Exception { + Tomcat server = new Tomcat(); + try { + StandardHost host = new StandardHost(); + host.setName("localhost"); + server.setHost(host); + server.setPort(0); + server.start(); + + this.port = server.getConnector().getLocalPort(); + + Context context = server.addContext("", null); + int i = 0; + for (Servlet servlet : servlets) { + server.addServlet("", "servlet" + i, servlet); + context.addServletMappingDecoded("/" + i + "/*", "servlet" + i); + i++; + } + + doWithTomcat.call(); + + } finally { + server.stop(); + server.destroy(); + + } + } + + private void checkMbeansInitialState() { + assertThat(registry.get("tomcat.global.sent").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.global.received").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.global.error").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.global.request").functionTimer().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.global.request").functionTimer().totalTime(TimeUnit.MILLISECONDS)).isEqualTo(0.0); + assertThat(registry.get("tomcat.global.request.max").timeGauge().value(TimeUnit.MILLISECONDS)).isEqualTo(0.0); + assertThat(registry.get("tomcat.threads.config.max").gauge().value()).isGreaterThan(0.0); + assertThat(registry.get("tomcat.threads.busy").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.threads.current").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.connections.current").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.connections.keepalive.current").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.connections.config.max").gauge().value()).isGreaterThan(0.0); + assertThat(registry.get("tomcat.cache.access").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.cache.hit").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.servlet.error").functionCounter().count()).isEqualTo(0.0); + } + + private void checkMbeansAfterRequests(long expectedSentBytes) { + await().atMost(5, TimeUnit.SECONDS) + .until(() -> registry.get("tomcat.global.sent").functionCounter().count() == expectedSentBytes); + assertThat(registry.get("tomcat.global.received").functionCounter().count()).isEqualTo(10.0); + assertThat(registry.get("tomcat.global.error").functionCounter().count()).isEqualTo(1.0); + assertThat(registry.get("tomcat.global.request").functionTimer().count()).isEqualTo(2.0); + assertThat(registry.get("tomcat.global.request").functionTimer().totalTime(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + assertThat(registry.get("tomcat.global.request.max").timeGauge().value(TimeUnit.MILLISECONDS)).isGreaterThanOrEqualTo(PROCESSING_TIME_IN_MILLIS); + assertThat(registry.get("tomcat.threads.config.max").gauge().value()).isGreaterThan(0.0); + assertThat(registry.get("tomcat.threads.busy").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.threads.current").gauge().value()).isGreaterThan(0.0); + assertThat(registry.get("tomcat.connections.current").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.connections.keepalive.current").gauge().value()).isGreaterThanOrEqualTo(0.0); + assertThat(registry.get("tomcat.connections.config.max").gauge().value()).isGreaterThan(0.0); + assertThat(registry.get("tomcat.cache.access").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.cache.hit").functionCounter().count()).isEqualTo(0.0); + assertThat(registry.get("tomcat.servlet.error").functionCounter().count()).isEqualTo(1.0); + } +} diff --git a/micrometer-binders/src/test/resources/binder/logging/log4j2-async-logger.xml b/micrometer-binders/src/test/resources/binder/logging/log4j2-async-logger.xml new file mode 100644 index 0000000000..050b6c664c --- /dev/null +++ b/micrometer-binders/src/test/resources/binder/logging/log4j2-async-logger.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/micrometer-binders/src/test/resources/binder/logging/log4j2-root-logger-additivity-false.xml b/micrometer-binders/src/test/resources/binder/logging/log4j2-root-logger-additivity-false.xml new file mode 100644 index 0000000000..b17e7c0bce --- /dev/null +++ b/micrometer-binders/src/test/resources/binder/logging/log4j2-root-logger-additivity-false.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/micrometer-binders/src/test/resources/logback.xml b/micrometer-binders/src/test/resources/logback.xml new file mode 100644 index 0000000000..bd9a31725b --- /dev/null +++ b/micrometer-binders/src/test/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java index ab8f0cce81..7ac9d949ba 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinder.java @@ -31,10 +31,12 @@ * Having this common base set of metrics ensures that you can reason about basic cache performance * in a dimensional slice that spans different cache implementations in your application. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.CacheMeterBinder} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public abstract class CacheMeterBinder implements MeterBinder { private final WeakReference cacheRef; private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java index 5d9ad894dd..e3bb763c54 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineCacheMetrics.java @@ -36,11 +36,13 @@ * CaffeineCacheMetrics.monitor(registry, cache, "mycache", "region", "test"); * } * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.CaffeineCacheMetrics} * @author Clint Checketts * @see CaffeineStatsCounter */ @NonNullApi @NonNullFields +@Deprecated public class CaffeineCacheMetrics> extends CacheMeterBinder { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounter.java index cdb1ce9aa9..44aa505357 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/CaffeineStatsCounter.java @@ -51,6 +51,7 @@ * .build(); * } * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.CaffeineStatsCounter} * @author Ben Manes * @author John Karp * @author Johnny Lim @@ -59,6 +60,7 @@ */ @NonNullApi @NonNullFields +@Deprecated public final class CaffeineStatsCounter implements StatsCounter { private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java index 2f477c3fac..13db3c37e8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/EhCache2Metrics.java @@ -29,10 +29,12 @@ /** * Collect metrics on EhCache caches, including detailed metrics on transactions and storage space. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.EhCache2Metrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class EhCache2Metrics extends CacheMeterBinder { public EhCache2Metrics(Ehcache cache, Iterable tags) { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java index 531333950b..628cbb56b8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/GuavaCacheMetrics.java @@ -28,10 +28,12 @@ import java.util.function.ToLongFunction; /** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.GuavaCacheMetrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class GuavaCacheMetrics> extends CacheMeterBinder { /** * Record metrics on a Guava cache. You must call {@link CacheBuilder#recordStats()} prior to building the cache diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetrics.java index b3a78648bd..f05daedb7a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastCacheMetrics.java @@ -29,10 +29,12 @@ /** * Collect metrics on Hazelcast caches, including detailed metrics on storage space, near cache usage, and timings. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.HazelcastCacheMetrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class HazelcastCacheMetrics extends CacheMeterBinder { private final HazelcastIMapAdapter cache; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java index beaa3f560e..d567b7c0f8 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/HazelcastIMapAdapter.java @@ -29,7 +29,9 @@ * same time. Dynamically checks which Hazelcast version is on the classpath and resolves the right classes. * * @implNote Note that {@link MethodHandle} is used, so the performance does not suffer. + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.HazelcastIMapAdapter} */ +@Deprecated class HazelcastIMapAdapter { private static final Class CLASS_I_MAP = resolveOneOf("com.hazelcast.map.IMap", "com.hazelcast.core.IMap"); private static final Class CLASS_LOCAL_MAP = resolveOneOf("com.hazelcast.map.LocalMapStats", "com.hazelcast.monitor.LocalMapStats"); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java index b4926dd4d3..15676155db 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/JCacheMetrics.java @@ -36,10 +36,12 @@ * Note that JSR-107 does not provide any insight into the size or estimated size of the cache, so * the size metric of a JCache cache will always report 0. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.cache.JCacheMetrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class JCacheMetrics> extends CacheMeterBinder { // VisibleForTesting @Nullable diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/package-info.java index 7051bdb6cc..b93e2f44a3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/package-info.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/cache/package-info.java @@ -14,6 +14,7 @@ * limitations under the License. */ @NonNullApi +@Deprecated package io.micrometer.core.instrument.binder.cache; import io.micrometer.core.lang.NonNullApi; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/commonspool2/CommonsObjectPool2Metrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/commonspool2/CommonsObjectPool2Metrics.java index 24cc32daff..d6cf5994c7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/commonspool2/CommonsObjectPool2Metrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/commonspool2/CommonsObjectPool2Metrics.java @@ -40,9 +40,11 @@ * Apache Commons Pool 2.x metrics collected from metrics exposed via the MBeanServer. * Metrics are exposed for each object pool. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.commonspool2.CommonsObjectPool2Metrics} * @author Chao Chang * @since 1.6.0 */ +@Deprecated public class CommonsObjectPool2Metrics implements MeterBinder, AutoCloseable { private static final InternalLogger log = InternalLoggerFactory.getInstance(CommonsObjectPool2Metrics.class); private static final String JMX_DOMAIN = "org.apache.commons.pool2"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/DatabaseTableMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/DatabaseTableMetrics.java index f5231b1d0b..d892bdc3df 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/DatabaseTableMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/DatabaseTableMetrics.java @@ -32,10 +32,12 @@ import java.util.function.ToDoubleFunction; /** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.db.DatabaseTableMetrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class DatabaseTableMetrics implements MeterBinder { private final DataSource dataSource; private final String query; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java index b2b680359c..a6f4480ae2 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/JooqExecuteListener.java @@ -27,6 +27,10 @@ import java.util.function.Supplier; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.db.JooqExecuteListener} + */ +@Deprecated class JooqExecuteListener extends DefaultExecuteListener { private final MeterRegistry registry; private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/MetricsDSLContext.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/MetricsDSLContext.java index be245a3346..2cf2ba0282 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/MetricsDSLContext.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/MetricsDSLContext.java @@ -62,11 +62,13 @@ * * This requires jOOQ 3.14.0 or later. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.db.MetricsDSLContext} * @author Jon Schneider * @author Johnny Lim * @since 1.4.0 */ @Incubating(since = "1.4.0") +@Deprecated public class MetricsDSLContext implements DSLContext { private final DSLContext context; private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/PostgreSQLDatabaseMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/PostgreSQLDatabaseMetrics.java index 896db08582..46877bca47 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/PostgreSQLDatabaseMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/PostgreSQLDatabaseMetrics.java @@ -33,6 +33,7 @@ /** * {@link MeterBinder} for a PostgreSQL database. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.db.PostgreSQLDatabaseMetrics} * @author Kristof Depypere * @author Jon Schneider * @author Johnny Lim @@ -41,6 +42,7 @@ */ @NonNullApi @NonNullFields +@Deprecated public class PostgreSQLDatabaseMetrics implements MeterBinder { private static final String SELECT = "SELECT "; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/package-info.java new file mode 100644 index 0000000000..53dc160667 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/db/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright 2017 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.db; + diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/AbstractMetricCollectingInterceptor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/AbstractMetricCollectingInterceptor.java index e784111d53..ec2c066074 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/AbstractMetricCollectingInterceptor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/AbstractMetricCollectingInterceptor.java @@ -36,9 +36,11 @@ /** * An abstract gRPC interceptor that will collect metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.AbstractMetricCollectingInterceptor} * @author Daniel Theuke (daniel.theuke@heuboe.de) * @since 1.7.0 */ +@Deprecated public abstract class AbstractMetricCollectingInterceptor { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCall.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCall.java index c03b5c4aa0..fefa85d63a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCall.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCall.java @@ -26,10 +26,12 @@ /** * A simple forwarding client call that collects metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingClientCall} * @param The type of message sent one or more times to the server. * @param The type of message received one or more times from the server. * @author Daniel Theuke (daniel.theuke@heuboe.de) */ +@Deprecated class MetricCollectingClientCall extends SimpleForwardingClientCall { private final Counter requestCounter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCallListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCallListener.java index 93351bc742..9b38eb12df 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCallListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientCallListener.java @@ -26,9 +26,11 @@ /** * A simple forwarding client call listener that collects metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingClientCallListener} * @param The type of message received one or more times from the server. * @author Daniel Theuke (daniel.theuke@heuboe.de) */ +@Deprecated class MetricCollectingClientCallListener extends SimpleForwardingClientCallListener { private final Counter responseCounter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientInterceptor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientInterceptor.java index ec2d383e79..2741e7d5fb 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientInterceptor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingClientInterceptor.java @@ -44,9 +44,11 @@ * channel.newCall(method, options); * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingClientInterceptor} * @author Daniel Theuke (daniel.theuke@heuboe.de) * @since 1.7.0 */ +@Deprecated public class MetricCollectingClientInterceptor extends AbstractMetricCollectingInterceptor implements ClientInterceptor { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCall.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCall.java index a021d09cc8..7f1bfed1a9 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCall.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCall.java @@ -28,7 +28,9 @@ * @param The type of message received one or more times from the client. * @param The type of message sent one or more times to the client. * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingServerCall} */ +@Deprecated class MetricCollectingServerCall extends SimpleForwardingServerCall { private final Counter responseCounter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCallListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCallListener.java index 626f09e4ab..c4639f73b9 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCallListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerCallListener.java @@ -29,7 +29,9 @@ * * @param The type of message received one or more times from the client. * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingServerCallListener} */ +@Deprecated class MetricCollectingServerCallListener extends SimpleForwardingServerCallListener { private final Counter requestCounter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerInterceptor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerInterceptor.java index b76073b6e9..1f6c478a37 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerInterceptor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/MetricCollectingServerInterceptor.java @@ -48,9 +48,11 @@ * server.start() * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.grpc.MetricCollectingServerInterceptor} * @author Daniel Theuke (daniel.theuke@heuboe.de) * @since 1.7.0 */ +@Deprecated public class MetricCollectingServerInterceptor extends AbstractMetricCollectingInterceptor implements ServerInterceptor { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/package-info.java index 56c46e992a..10b9f6028e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/package-info.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/grpc/package-info.java @@ -20,4 +20,5 @@ * {@link io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor} * for usage examples. */ +@Deprecated package io.micrometer.core.instrument.binder.grpc; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServletRequestTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServletRequestTagsProvider.java index 632a70704c..6f587c8c26 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServletRequestTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/DefaultHttpServletRequestTagsProvider.java @@ -25,10 +25,12 @@ /** * Default {@link HttpServletRequestTagsProvider}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.http.DefaultHttpServletRequestTagsProvider} * @author Jon Schneider * @since 1.4.0 */ @Incubating(since = "1.4.0") +@Deprecated public class DefaultHttpServletRequestTagsProvider implements HttpServletRequestTagsProvider { @Override public Iterable getTags(HttpServletRequest request, HttpServletResponse response) { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpRequestTags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpRequestTags.java index 45230e514f..f40dabfad2 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpRequestTags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpRequestTags.java @@ -25,10 +25,12 @@ /** * Tags for HTTP requests. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.http.HttpRequestTags} * @author Jon Schneider * @since 1.4.0 */ @Incubating(since = "1.4.0") +@Deprecated public class HttpRequestTags { private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServletRequestTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServletRequestTagsProvider.java index 249f615334..930eb1ef17 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServletRequestTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/HttpServletRequestTagsProvider.java @@ -24,11 +24,13 @@ /** * Provides {@link Tag Tags} for HTTP Servlet request handling. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.http.HttpServletRequestTagsProvider} * @author Jon Schneider * @since 1.4.0 */ @Incubating(since = "1.4.0") @FunctionalInterface +@Deprecated public interface HttpServletRequestTagsProvider { /** * Provides tags to be associated with metrics for the given {@code request} and diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/Outcome.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/Outcome.java index eae67ad1b5..b7ff43cb43 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/Outcome.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/Outcome.java @@ -20,9 +20,11 @@ /** * The outcome of an HTTP request. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.http.Outcome} * @author Andy Wilkinson * @since 1.4.0 */ +@Deprecated public enum Outcome { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/package-info.java index 0235005519..808810e34e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/package-info.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/http/package-info.java @@ -13,8 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@Deprecated package io.micrometer.core.instrument.binder.http; - -/** - * Provides a general-purpose mechanism for injecting a tags provider into various HTTP frameworks. - */ diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/DefaultUriMapper.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/DefaultUriMapper.java index 9ce75e909d..cd41d3d2c3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/DefaultUriMapper.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/DefaultUriMapper.java @@ -23,9 +23,11 @@ /** * Extracts the URI pattern from the predefined request header, {@value DefaultUriMapper#URI_PATTERN_HEADER} if available. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.httpcomponents.DefaultUriMapper} * @author Benjamin Hubert * @since 1.4.0 */ +@Deprecated public class DefaultUriMapper implements Function { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/HttpContextUtils.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/HttpContextUtils.java index 44382df5d6..3855a4547a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/HttpContextUtils.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/HttpContextUtils.java @@ -20,6 +20,10 @@ import org.apache.http.conn.routing.HttpRoute; import org.apache.http.protocol.HttpContext; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.httpcomponents.HttpContextUtils} + */ +@Deprecated class HttpContextUtils { static Tags generateTagsForRoute(HttpContext context) { String targetScheme = "UNKNOWN"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpClientInterceptor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpClientInterceptor.java index 880655495c..fb4b8eca76 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpClientInterceptor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpClientInterceptor.java @@ -44,10 +44,12 @@ * .build(); * } * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.httpcomponents.MicrometerHttpClientInterceptor} * @author Jon Schneider * @since 1.4.0 */ @Incubating(since = "1.4.0") +@Deprecated public class MicrometerHttpClientInterceptor { private static final String METER_NAME = "httpcomponents.httpclient.request"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutor.java index 4f6cd18858..d28274bfb0 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/MicrometerHttpRequestExecutor.java @@ -46,11 +46,13 @@ * .build(); * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.httpcomponents.MicrometerHttpRequestExecutor} * @author Benjamin Hubert (benjamin.hubert@willhaben.at) * @author Tommy Ludwig * @since 1.2.0 */ @Incubating(since = "1.2.0") +@Deprecated public class MicrometerHttpRequestExecutor extends HttpRequestExecutor { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java index ca07815894..921a590a8f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/PoolingHttpClientConnectionManagerMetricsBinder.java @@ -30,9 +30,11 @@ *

* It monitors the overall connection pool state. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.httpcomponents.PoolingHttpClientConnectionManagerMetricsBinder} * @author Benjamin Hubert (benjamin.hubert@willhaben.at) * @since 1.3.0 */ +@Deprecated public class PoolingHttpClientConnectionManagerMetricsBinder implements MeterBinder { private final ConnPoolControl connPoolControl; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/package-info.java new file mode 100644 index 0000000000..73eae75489 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/httpcomponents/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.httpcomponents; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/HystrixMetricsBinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/HystrixMetricsBinder.java index 1dffa34fcb..39e2880fb2 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/HystrixMetricsBinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/HystrixMetricsBinder.java @@ -26,8 +26,12 @@ import io.micrometer.core.lang.NonNullApi; import io.micrometer.core.lang.NonNullFields; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.hystrix.HystrixMetricsBinder} + */ @NonNullApi @NonNullFields +@Deprecated public class HystrixMetricsBinder implements MeterBinder { @Override diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisher.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisher.java index 59503e0645..d5d363316f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisher.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisher.java @@ -25,10 +25,12 @@ import io.micrometer.core.lang.NonNullFields; /** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.hystrix.MicrometerMetricsPublisher} * @author Clint Checketts */ @NonNullApi @NonNullFields +@Deprecated public class MicrometerMetricsPublisher extends HystrixMetricsPublisher { private final MeterRegistry registry; private HystrixMetricsPublisher metricsPublisher; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java index 43e332b7eb..c99a312b57 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherCommand.java @@ -30,10 +30,12 @@ import java.util.concurrent.TimeUnit; /** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.hystrix.MicrometerMetricsPublisherCommand} * @author Clint Checketts */ @NonNullApi @NonNullFields +@Deprecated public class MicrometerMetricsPublisherCommand implements HystrixMetricsPublisherCommand { private static final InternalLogger LOG = InternalLoggerFactory.getInstance(MicrometerMetricsPublisherCommand.class); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherThreadPool.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherThreadPool.java index 1e0ec41ff7..108f3610e5 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherThreadPool.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/MicrometerMetricsPublisherThreadPool.java @@ -31,10 +31,12 @@ /** * Micrometer publisher for Hystrix thread pool metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.hystrix.MicrometerMetricsPublisherThreadPool} * @since 1.2.0 */ @NonNullApi @NonNullFields +@Deprecated public class MicrometerMetricsPublisherThreadPool implements HystrixMetricsPublisherThreadPool { private static final String NAME_HYSTRIX_THREADPOOL = "hystrix.threadpool"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/package-info.java new file mode 100644 index 0000000000..8e48cb8d05 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/hystrix/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.hystrix; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/AnnotationFinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/AnnotationFinder.java index a917217fde..8ae82be022 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/AnnotationFinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/AnnotationFinder.java @@ -18,6 +18,10 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.AnnotationFinder} + */ +@Deprecated public interface AnnotationFinder { AnnotationFinder DEFAULT = new AnnotationFinder() { }; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/DefaultJerseyTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/DefaultJerseyTagsProvider.java index f8f410e159..8121cbfe8f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/DefaultJerseyTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/DefaultJerseyTagsProvider.java @@ -22,11 +22,13 @@ /** * Default implementation for {@link JerseyTagsProvider}. - * + * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.DefaultJerseyTagsProvider} * @author Michael Weirauch * @author Johnny Lim * @since 1.8.0 */ +@Deprecated public final class DefaultJerseyTagsProvider implements JerseyTagsProvider { @Override diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java index 6e531d68e2..629325b90a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTags.java @@ -31,10 +31,12 @@ * Factory methods for {@link Tag Tags} associated with a request-response exchange that * is handled by Jersey server. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.JerseyTags} * @author Michael Weirauch * @author Johnny Lim * @since 1.8.0 */ +@Deprecated public final class JerseyTags { private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTagsProvider.java index be0ea4cb21..4e7929b7ba 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/JerseyTagsProvider.java @@ -20,10 +20,12 @@ /** * Provides {@link Tag Tags} for Jersey request metrics. - * + * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.JerseyTagsProvider} * @author Michael Weirauch * @since 1.8.0 */ +@Deprecated public interface JerseyTagsProvider { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsApplicationEventListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsApplicationEventListener.java index 3d7ed0c234..fe2904a482 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsApplicationEventListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsApplicationEventListener.java @@ -27,9 +27,11 @@ * The Micrometer {@link ApplicationEventListener} which registers * {@link RequestEventListener} for instrumenting Jersey server requests. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.MetricsApplicationEventListener} * @author Michael Weirauch * @since 1.8.0 */ +@Deprecated public class MetricsApplicationEventListener implements ApplicationEventListener { private final MeterRegistry meterRegistry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListener.java index e29566d083..7f19421ccb 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/MetricsRequestEventListener.java @@ -32,10 +32,12 @@ /** * {@link RequestEventListener} recording timings for Jersey server requests. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.MetricsRequestEventListener} * @author Michael Weirauch * @author Jon Schneider * @since 1.8.0 */ +@Deprecated public class MetricsRequestEventListener implements RequestEventListener { private final Map shortTaskSample = Collections diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/TimedFinder.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/TimedFinder.java index 8ee1a87081..4490cb98d3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/TimedFinder.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/TimedFinder.java @@ -24,6 +24,10 @@ import java.util.Set; import java.util.stream.Collectors; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jersey.server.TimedFinder} + */ +@Deprecated class TimedFinder { private final AnnotationFinder annotationFinder; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/package-info.java new file mode 100644 index 0000000000..cac3a566e3 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jersey/server/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.jersey.server; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/InstrumentedQueuedThreadPool.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/InstrumentedQueuedThreadPool.java index a2a4f27fce..8325b42736 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/InstrumentedQueuedThreadPool.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/InstrumentedQueuedThreadPool.java @@ -30,9 +30,11 @@ * // ... * } * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.InstrumentedQueuedThreadPool} * @since 1.1.0 * @see JettyServerThreadPoolMetrics */ +@Deprecated public class InstrumentedQueuedThreadPool extends QueuedThreadPool { private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientMetrics.java index 9938043d1c..88a50f7f6f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientMetrics.java @@ -32,10 +32,12 @@ * configured as a {@link org.eclipse.jetty.client.api.Request.Listener Request.Listener}. * Incubating in case there emerges a better way to handle path variable detection. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettyClientMetrics} * @author Jon Schneider * @since 1.5.0 */ @Incubating(since = "1.5.0") +@Deprecated public class JettyClientMetrics implements Request.Listener { private final MeterRegistry registry; private final JettyClientTagsProvider tagsProvider; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTags.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTags.java index 99514cf558..cb1e2bf887 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTags.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTags.java @@ -30,9 +30,11 @@ * Factory methods for {@link Tag Tags} associated with a request-response exchange that * is handled by Jetty {@link org.eclipse.jetty.client.HttpClient}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettyClientTags} * @author Jon Schneider * @since 1.5.0 */ +@Deprecated public final class JettyClientTags { private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTagsProvider.java index 5eb07bd2ea..dc1744d62d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyClientTagsProvider.java @@ -24,10 +24,12 @@ * Provides {@link Tag Tags} for Jetty {@link org.eclipse.jetty.client.HttpClient} request metrics. * Incubating in case there emerges a better way to handle path variable detection. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettyClientTagsProvider} * @author Jon Schneider * @since 1.5.0 */ @Incubating(since = "1.5.0") +@Deprecated public interface JettyClientTagsProvider { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java index 3bb8ef3dc4..a564aabd5c 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyConnectionMetrics.java @@ -48,9 +48,11 @@ * * Alternatively, configure on all connectors with {@link JettyConnectionMetrics#addToAllConnectors(Server, MeterRegistry, Iterable)}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettyConnectionMetrics} * @author Jon Schneider * @since 1.4.0 */ +@Deprecated public class JettyConnectionMetrics extends AbstractLifeCycle implements Connection.Listener { private final MeterRegistry registry; private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyServerThreadPoolMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyServerThreadPoolMetrics.java index f2b920a213..420e64b7d4 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyServerThreadPoolMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyServerThreadPoolMetrics.java @@ -35,12 +35,14 @@ * } * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettyServerThreadPoolMetrics} * @author Manabu Matsuzaki * @author Andy Wilkinson * @author Johnny Lim * @since 1.1.0 * @see InstrumentedQueuedThreadPool */ +@Deprecated public class JettyServerThreadPoolMetrics implements MeterBinder { private final ThreadPool threadPool; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettySslHandshakeMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettySslHandshakeMetrics.java index 8396556dda..9f71f46b62 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettySslHandshakeMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettySslHandshakeMetrics.java @@ -41,10 +41,12 @@ * * Alternatively, configure on all connectors with {@link JettySslHandshakeMetrics#addToAllConnectors(Server, MeterRegistry, Iterable)}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.JettySslHandshakeMetrics} * @author John Karp * @author Johnny Lim * @since 1.5.0 */ +@Deprecated public class JettySslHandshakeMetrics implements SslHandshakeListener { private static final String METER_NAME = "jetty.ssl.handshakes"; private static final String DESCRIPTION = "SSL/TLS handshakes"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyStatisticsMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyStatisticsMetrics.java index 4478f51273..84cceb7376 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyStatisticsMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/JettyStatisticsMetrics.java @@ -27,7 +27,7 @@ import java.util.function.ToLongFunction; /** - * @deprecated Since 1.4.0. Use {@link TimedHandler} instead. + * @deprecated Since 1.4.0. Use {@code io.micrometer.binder.jetty.TimedHandler} instead. */ @Deprecated @NonNullApi diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/OnCompletionAsyncListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/OnCompletionAsyncListener.java index dbe62acc63..cf451ba1e3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/OnCompletionAsyncListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/OnCompletionAsyncListener.java @@ -22,7 +22,10 @@ * {@link AsyncListener} that calls back to the handler. This class * uses only object references to work around * WFLY-13345 + * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.OnCompletionAsyncListener} */ +@Deprecated class OnCompletionAsyncListener implements AsyncListener { private final Object handler; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/TimedHandler.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/TimedHandler.java index 23c24b4497..1eedb0d8ac 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/TimedHandler.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/TimedHandler.java @@ -42,11 +42,13 @@ /** * Adapted from Jetty's StatisticsHandler. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jetty.TimedHandler} * @author Jon Schneider * @since 1.4.0 */ @NonNullApi @NonNullFields +@Deprecated public class TimedHandler extends HandlerWrapper implements Graceful { private static final String SAMPLE_REQUEST_TIMER_ATTRIBUTE = "__micrometer_timer_sample"; private static final String SAMPLE_REQUEST_LONG_TASK_TIMER_ATTRIBUTE = "__micrometer_ltt_sample"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/package-info.java new file mode 100644 index 0000000000..0b8d908941 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jetty/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.jetty; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jpa/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jpa/package-info.java new file mode 100644 index 0000000000..c575c82722 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jpa/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.jpa; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java index 4bd04bffdf..8282e7902e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ClassLoaderMetrics.java @@ -29,8 +29,12 @@ import static java.util.Collections.emptyList; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.ClassLoaderMetrics} + */ @NonNullApi @NonNullFields +@Deprecated public class ClassLoaderMetrics implements MeterBinder { private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/DiskSpaceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/DiskSpaceMetrics.java index 1c71fcac2a..751ebcca04 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/DiskSpaceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/DiskSpaceMetrics.java @@ -34,7 +34,7 @@ * * @author jmcshane * @author Johnny Lim - * @deprecated use {@link io.micrometer.core.instrument.binder.system.DiskSpaceMetrics} instead. + * @deprecated use {@code io.micrometer.binder.system.DiskSpaceMetrics} instead. */ @Incubating(since = "1.1.0") @NonNullApi diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java index 1b57d91f1f..c2d3f359e1 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/ExecutorServiceMetrics.java @@ -42,12 +42,14 @@ * a wrapper type for {@link ExecutorService}, like {@link TimedExecutorService}. Make sure to pass the underlying, * unwrapped ExecutorService to this MeterBinder, if it is wrapped in another type. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.ExecutorServiceMetrics} * @author Jon Schneider * @author Clint Checketts * @author Johnny Lim */ @NonNullApi @NonNullFields +@Deprecated public class ExecutorServiceMetrics implements MeterBinder { private static boolean allowIllegalReflectiveAccess = true; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmCompilationMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmCompilationMetrics.java index fd7a833567..789e26f5bb 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmCompilationMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmCompilationMetrics.java @@ -32,10 +32,12 @@ /** * {@link MeterBinder} for JVM compilation metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmCompilationMetrics} * @since 1.4.0 */ @NonNullApi @NonNullFields +@Deprecated public class JvmCompilationMetrics implements MeterBinder { private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetrics.java index 02412c6034..b8695c9fc7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmGcMetrics.java @@ -51,12 +51,14 @@ * This provides metrics for OpenJDK garbage collectors (serial, parallel, G1, Shenandoah, ZGC) * and for OpenJ9 garbage collectors (gencon, balanced, opthruput, optavgpause, metronome). * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmGcMetrics} * @author Jon Schneider * @author Tommy Ludwig * @see GarbageCollectorMXBean */ @NonNullApi @NonNullFields +@Deprecated public class JvmGcMetrics implements MeterBinder, AutoCloseable { private static final InternalLogger log = InternalLoggerFactory.getInstance(JvmGcMetrics.class); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmHeapPressureMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmHeapPressureMetrics.java index 3bf23a15a2..1a1931258e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmHeapPressureMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmHeapPressureMetrics.java @@ -44,9 +44,11 @@ * Provides methods to access measurements of low pool memory and heavy GC overhead as described in * TeamCity's Memory Monitor. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmHeapPressureMetrics} * @author Jon Schneider * @since 1.4.0 */ +@Deprecated public class JvmHeapPressureMetrics implements MeterBinder, AutoCloseable { private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmInfoMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmInfoMetrics.java index 0e084cc9a3..a73c3a487d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmInfoMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmInfoMetrics.java @@ -22,9 +22,11 @@ /** * {@link MeterBinder} for JVM information. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmInfoMetrics} * @author Erin Schnabel * @since 1.7.0 */ +@Deprecated public class JvmInfoMetrics implements MeterBinder { @Override diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemory.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemory.java index f99e74c9a1..b539a12694 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemory.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemory.java @@ -24,6 +24,10 @@ import java.util.function.ToLongFunction; import java.util.stream.Stream; +/** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmMemory} + */ +@Deprecated class JvmMemory { private JvmMemory() { diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java index d6c75b901f..0697262081 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmMemoryMetrics.java @@ -36,6 +36,7 @@ /** * Record metrics that report utilization of various memory and buffer pools. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmMemoryMetrics} * @author Jon Schneider * @author Johnny Lim * @see MemoryPoolMXBean @@ -43,6 +44,7 @@ */ @NonNullApi @NonNullFields +@Deprecated public class JvmMemoryMetrics implements MeterBinder { private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java index b90807d150..4ddbfc7c2d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/JvmThreadMetrics.java @@ -33,11 +33,13 @@ /** * {@link MeterBinder} for JVM threads. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.jvm.JvmThreadMetrics} * @author Jon Schneider * @author Johnny Lim */ @NonNullApi @NonNullFields +@Deprecated public class JvmThreadMetrics implements MeterBinder { private final Iterable tags; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/package-info.java new file mode 100644 index 0000000000..47d9e744c8 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/jvm/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.jvm; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java index ba4aff8396..6fd1ab4375 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaClientMetrics.java @@ -31,6 +31,7 @@ *

* Meter names have the following convention: {@code kafka.(metric_group).(metric_name)} * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.kafka.KafkaClientMetrics} * @author Jorge Quilcate * @see Kakfa monitoring * documentation @@ -39,6 +40,7 @@ @Incubating(since = "1.4.0") @NonNullApi @NonNullFields +@Deprecated public class KafkaClientMetrics extends KafkaMetrics { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaConsumerMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaConsumerMetrics.java index 81a45d4bcf..413afaacbd 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaConsumerMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaConsumerMetrics.java @@ -49,7 +49,7 @@ * @author Johnny Lim * @see Kakfa monitoring documentation * @since 1.1.0 - * @deprecated use {@link KafkaClientMetrics} instead since 1.4.0 + * @deprecated since 1.4.0, use {@code io.micrometer.binder.kafka.KafkaClientMetrics} instead. */ @Incubating(since = "1.1.0") @NonNullApi diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java index 84b31da40d..ed90ecb4ce 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaMetrics.java @@ -51,6 +51,7 @@ /** * Kafka metrics binder. This should be closed on application shutdown to clean up resources. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.kafka.KafkaMetrics} * @author Jorge Quilcate * @see Kakfa monitoring * documentation @@ -59,6 +60,7 @@ @Incubating(since = "1.4.0") @NonNullApi @NonNullFields +@Deprecated class KafkaMetrics implements MeterBinder, AutoCloseable { private static final InternalLogger log = InternalLoggerFactory.getInstance(KafkaMetrics.class); private static final WarnThenDebugLogger warnThenDebugLogger = new WarnThenDebugLogger(KafkaMetrics.class); diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java index d774a6f350..f838285008 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/KafkaStreamsMetrics.java @@ -29,6 +29,7 @@ *

* Meter names have the following convention: {@code kafka.(metric_group).(metric_name)} * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.kafka.KafkaStreamsMetrics} * @author Jorge Quilcate * @see Kakfa monitoring * documentation @@ -37,6 +38,7 @@ @Incubating(since = "1.4.0") @NonNullApi @NonNullFields +@Deprecated public class KafkaStreamsMetrics extends KafkaMetrics { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/package-info.java new file mode 100644 index 0000000000..1a9b8be1f0 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/kafka/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.kafka; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java index c854c2c386..3c0cc03a18 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/Log4j2Metrics.java @@ -41,12 +41,14 @@ /** * {@link MeterBinder} for Apache Log4j 2. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.logging.Log4j2Metrics} * @author Steven Sheehy * @author Johnny Lim * @since 1.1.0 */ @NonNullApi @NonNullFields +@Deprecated public class Log4j2Metrics implements MeterBinder, AutoCloseable { private static final String METER_NAME = "log4j2.events"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/LogbackMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/LogbackMetrics.java index ab49ebd811..13ee53e9b3 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/LogbackMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/LogbackMetrics.java @@ -37,10 +37,12 @@ import static java.util.Collections.emptyList; /** + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.logging.LogbackMetrics} * @author Jon Schneider */ @NonNullApi @NonNullFields +@Deprecated public class LogbackMetrics implements MeterBinder, AutoCloseable { static ThreadLocal ignoreMetrics = new ThreadLocal<>(); @@ -129,6 +131,7 @@ public void close() { @NonNullApi @NonNullFields +@Deprecated class MetricsTurboFilter extends TurboFilter { private final Counter errorCounter; private final Counter warnCounter; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/package-info.java new file mode 100644 index 0000000000..2a18cdeb0e --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/logging/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.logging; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoCommandTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoCommandTagsProvider.java index ea7bbdde14..75c936ed98 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoCommandTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoCommandTagsProvider.java @@ -36,9 +36,11 @@ /** * Default implementation for {@link MongoCommandTagsProvider}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.DefaultMongoCommandTagsProvider} * @author Chris Bono * @since 1.7.0 */ +@Deprecated public class DefaultMongoCommandTagsProvider implements MongoCommandTagsProvider { // See https://docs.mongodb.com/manual/reference/command for the command reference diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java index dcfc869c33..01072c4c55 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/DefaultMongoConnectionPoolTagsProvider.java @@ -22,9 +22,11 @@ /** * Default implementation for {@link MongoConnectionPoolTagsProvider}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.DefaultMongoConnectionPoolTagsProvider} * @author Gustavo Monarin * @since 1.7.0 */ +@Deprecated public class DefaultMongoConnectionPoolTagsProvider implements MongoConnectionPoolTagsProvider { @Override diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoCommandTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoCommandTagsProvider.java index ed50f81769..699d98eb73 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoCommandTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoCommandTagsProvider.java @@ -22,10 +22,12 @@ /** * Provides {@link Tag Tags} for Mongo command metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.MongoCommandTagsProvider} * @author Chris Bono * @since 1.7.0 */ @FunctionalInterface +@Deprecated public interface MongoCommandTagsProvider { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoConnectionPoolTagsProvider.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoConnectionPoolTagsProvider.java index 606d00ead4..80027fe726 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoConnectionPoolTagsProvider.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoConnectionPoolTagsProvider.java @@ -21,10 +21,12 @@ /** * Provides {@link Tag Tags} for Mongo connection pool metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.MongoConnectionPoolTagsProvider} * @author Gustavo Monarin * @since 1.7.0 */ @FunctionalInterface +@Deprecated public interface MongoConnectionPoolTagsProvider { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsCommandListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsCommandListener.java index 6b9e5624f5..912dea2427 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsCommandListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsCommandListener.java @@ -28,6 +28,7 @@ /** * {@link CommandListener} for collecting command metrics from {@link MongoClient}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.MongoMetricsCommandListener} * @author Christophe Bornet * @author Chris Bono * @since 1.2.0 @@ -35,6 +36,7 @@ @NonNullApi @NonNullFields @Incubating(since = "1.2.0") +@Deprecated public class MongoMetricsCommandListener implements CommandListener { private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsConnectionPoolListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsConnectionPoolListener.java index 07723ff009..29ed68bae1 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsConnectionPoolListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/MongoMetricsConnectionPoolListener.java @@ -34,6 +34,7 @@ /** * {@link ConnectionPoolListener} for collecting connection pool metrics from {@link MongoClient}. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.mongodb.MongoMetricsConnectionPoolListener} * @author Christophe Bornet * @author Jonatan Ivanov * @since 1.2.0 @@ -42,6 +43,7 @@ @NonNullApi @NonNullFields @Incubating(since = "1.2.0") +@Deprecated public class MongoMetricsConnectionPoolListener implements ConnectionPoolListener { private static final String METRIC_PREFIX = "mongodb.driver.pool."; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/package-info.java new file mode 100644 index 0000000000..cd4f0d0bdf --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/mongodb/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.mongodb; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpConnectionPoolMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpConnectionPoolMetrics.java index aa87dc28c8..332c33f32f 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpConnectionPoolMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpConnectionPoolMetrics.java @@ -37,9 +37,11 @@ * new OkHttpConnectionPoolMetrics(connectionPool).bindTo(registry); * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.okhttp3.OkHttpConnectionPoolMetrics} * @author Ben Hubert * @since 1.6.0 */ +@Deprecated public class OkHttpConnectionPoolMetrics implements MeterBinder { private static final String DEFAULT_NAME_PREFIX = "okhttp.pool"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java index a138fd3ebf..884ad0800e 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListener.java @@ -46,6 +46,7 @@ * {@literal uri} tag or you can configure a {@link Builder#uriMapper(Function) URI mapper} to provide your own tag * values for {@literal uri} tag. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.okhttp3.OkHttpMetricsEventListener} * @author Bjarte S. Karlsen * @author Jon Schneider * @author Nurettin Yilmaz @@ -53,6 +54,7 @@ */ @NonNullApi @NonNullFields +@Deprecated public class OkHttpMetricsEventListener extends EventListener { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/package-info.java new file mode 100644 index 0000000000..e7b012b714 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/okhttp3/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.okhttp3; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/DiskSpaceMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/DiskSpaceMetrics.java index 9c6aa12d68..fd5fe0f83a 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/DiskSpaceMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/DiskSpaceMetrics.java @@ -31,12 +31,14 @@ /** * Record metrics that report disk space usage. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.system.DiskSpaceMetrics} * @author jmcshane * @author Johnny Lim * @since 1.8.0 */ @NonNullApi @NonNullFields +@Deprecated public class DiskSpaceMetrics implements MeterBinder { private final Iterable tags; private final File path; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetrics.java index d8a945c349..3b0544df98 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/FileDescriptorMetrics.java @@ -42,11 +42,13 @@ *

  • J9
  • * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.system.FileDescriptorMetrics} * @author Michael Weirauch * @author Tommy Ludwig */ @NonNullApi @NonNullFields +@Deprecated public class FileDescriptorMetrics implements MeterBinder { /** diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java index 81b7a8bdee..0826d22bf7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/ProcessorMetrics.java @@ -42,6 +42,7 @@ *
  • J9
  • * * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.system.ProcessorMetrics} * @author Jon Schneider * @author Michael Weirauch * @author Clint Checketts @@ -49,6 +50,7 @@ */ @NonNullApi @NonNullFields +@Deprecated public class ProcessorMetrics implements MeterBinder { /** List of public, exported interface class names from supported JVM implementations. */ diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/UptimeMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/UptimeMetrics.java index 4e3441a4b5..25dd6b1fa0 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/UptimeMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/UptimeMetrics.java @@ -31,10 +31,12 @@ /** * Uptime metrics. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.system.UptimeMetrics} * @author Michael Weirauch */ @NonNullApi @NonNullFields +@Deprecated public class UptimeMetrics implements MeterBinder { private final RuntimeMXBean runtimeMXBean; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/package-info.java new file mode 100644 index 0000000000..49c4a25e62 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/system/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.system; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/TomcatMetrics.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/TomcatMetrics.java index 096f86eb05..12af33b58d 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/TomcatMetrics.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/TomcatMetrics.java @@ -38,12 +38,14 @@ *

    Note: the {@link #close()} method should be called when the application shuts down * to clean up listeners this binder registers. * + * @deprecated Scheduled for removal in 2.0.0, please use {@code io.micrometer.binder.tomcat.TomcatMetrics} * @author Clint Checketts * @author Jon Schneider * @author Johnny Lim */ @NonNullApi @NonNullFields +@Deprecated public class TomcatMetrics implements MeterBinder, AutoCloseable { private static final String JMX_DOMAIN_EMBEDDED = "Tomcat"; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/package-info.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/package-info.java new file mode 100644 index 0000000000..cc2d6a9b26 --- /dev/null +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/binder/tomcat/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020 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. + */ +@Deprecated +package io.micrometer.core.instrument.binder.tomcat; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutor.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutor.java index a383fd4bdd..11956911d7 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutor.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutor.java @@ -25,7 +25,7 @@ /** * An {@link Executor} that is timed. This class is for internal use. * - * @see io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics + * @see "io.micrometer.binder.jvm.ExecutorServiceMetrics" */ public class TimedExecutor implements Executor { private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutorService.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutorService.java index 9f1cb1a206..7bc8539991 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutorService.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedExecutorService.java @@ -30,7 +30,7 @@ * An {@link java.util.concurrent.ExecutorService} that is timed. This class is for internal use. * * @author Jon Schneider - * @see io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics + * @see "io.micrometer.binder.jvm.ExecutorServiceMetrics" */ public class TimedExecutorService implements ExecutorService { private final MeterRegistry registry; diff --git a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedScheduledExecutorService.java b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedScheduledExecutorService.java index 257c770b33..cd6856530c 100644 --- a/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedScheduledExecutorService.java +++ b/micrometer-core/src/main/java/io/micrometer/core/instrument/internal/TimedScheduledExecutorService.java @@ -28,7 +28,7 @@ * * @author Sebastian Lövdahl * @since 1.3.0 - * @see io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics + * @see "io.micrometer.binder.jvm.ExecutorServiceMetrics" */ public class TimedScheduledExecutorService implements ScheduledExecutorService { private final MeterRegistry registry; diff --git a/micrometer-test/build.gradle b/micrometer-test/build.gradle index 479b48a5b9..f6babfcfdb 100644 --- a/micrometer-test/build.gradle +++ b/micrometer-test/build.gradle @@ -1,5 +1,6 @@ dependencies { api project(':micrometer-core') + api project(':micrometer-binders') api 'org.assertj:assertj-core' diff --git a/micrometer-test/src/main/java/io/micrometer/binder/cache/CacheMeterBinderCompatibilityKit.java b/micrometer-test/src/main/java/io/micrometer/binder/cache/CacheMeterBinderCompatibilityKit.java new file mode 100644 index 0000000000..9e191e92aa --- /dev/null +++ b/micrometer-test/src/main/java/io/micrometer/binder/cache/CacheMeterBinderCompatibilityKit.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017 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.binder.cache; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.lang.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jon Schneider + */ +public abstract class CacheMeterBinderCompatibilityKit { + private MeterRegistry registry = new SimpleMeterRegistry(); + private CacheMeterBinder binder; + protected C cache; + + /** + * The return value will be assigned to {@link #cache}. + * @return cache to use for tests + * @see #bindToRegistry() + */ + public abstract C createCache(); + + /** + * Performs any actions necessary to fully dereference the cache object. + */ + public void dereferenceCache() { + this.cache = null; + } + + /** + * @return A cache binder bound to a cache named "mycache". + */ + public abstract CacheMeterBinder binder(); + + public abstract void put(String key, String value); + + @Nullable + public abstract String get(String key); + + @BeforeEach + void bindToRegistry() { + this.cache = createCache(); + this.binder = binder(); + this.binder.bindTo(registry); + } + + @Test + void size() { + put("k", "v"); + assertThat(binder.size()).isIn(null, 1L); + + if (binder.size() != null) { + assertThat(registry.get("cache.size") + .tag("cache", "mycache") + .gauge().value()) + .isEqualTo(1); + } + } + + @Test + void puts() { + put("k", "v"); + assertThat(binder.putCount()).isEqualTo(1); + assertThat(registry.get("cache.puts").tag("cache", "mycache") + .functionCounter().count()) + .isEqualTo(1); + } + + @Test + void gets() { + put("k", "v"); + get("k"); + get("does.not.exist"); + + assertThat(binder.hitCount()).isEqualTo(1); + + assertThat(registry.get("cache.gets") + .tag("result", "hit") + .tag("cache", "mycache") + .functionCounter().count()) + .isEqualTo(1); + + if (binder.missCount() != null) { + // will be 2 for Guava/Caffeine caches where LoadingCache considers a get against a non-existent key a "miss" + assertThat(binder.missCount()).isIn(1L, 2L); + + assertThat(registry.get("cache.gets") + .tag("result", "miss") + .tag("cache", "mycache") + .functionCounter().count()) + .isIn(1.0, 2.0); + } + } + + @Test + void dereferencedCacheIsGarbageCollected() { + assertThat(binder.getCache()).isNotNull(); + + dereferenceCache(); + System.gc(); + + assertThat(binder.getCache()).isNull(); + } +} diff --git a/micrometer-test/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinderCompatibilityKit.java b/micrometer-test/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinderCompatibilityKit.java index fc9ad00e62..9191e4a378 100644 --- a/micrometer-test/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinderCompatibilityKit.java +++ b/micrometer-test/src/main/java/io/micrometer/core/instrument/binder/cache/CacheMeterBinderCompatibilityKit.java @@ -26,6 +26,7 @@ /** * @author Jon Schneider */ +@Deprecated public abstract class CacheMeterBinderCompatibilityKit { private MeterRegistry registry = new SimpleMeterRegistry(); private CacheMeterBinder binder; diff --git a/micrometer-test/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsCompatibilityTest.java b/micrometer-test/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsCompatibilityTest.java new file mode 100644 index 0000000000..f405c1ca0b --- /dev/null +++ b/micrometer-test/src/test/java/io/micrometer/binder/cache/CaffeineCacheMetricsCompatibilityTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.concurrent.atomic.AtomicReference; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import static java.util.Collections.emptyList; + +class CaffeineCacheMetricsCompatibilityTest extends CacheMeterBinderCompatibilityKit> { + private AtomicReference loadValue = new AtomicReference<>(); + + @Override + public LoadingCache createCache() { + return Caffeine.newBuilder() + .maximumSize(2) + .recordStats() + .executor(Runnable::run) + .build(key -> { + String val = loadValue.getAndSet(null); + if (val == null) + throw new Exception("don't load this key"); + return val; + }); + } + + @Override + public CacheMeterBinder> binder() { + return new CaffeineCacheMetrics<>(cache, "mycache", emptyList()); + } + + @Override + public void put(String key, String value) { + synchronized (this) { + loadValue.set(value); + cache.get(key); + } + } + + @Override + public String get(String key) { + try { + return cache.get(key); + } catch (Exception ignored) { + return null; + } + } +} diff --git a/micrometer-test/src/test/java/io/micrometer/binder/cache/EhCache2MetricsCompatibilityTest.java b/micrometer-test/src/test/java/io/micrometer/binder/cache/EhCache2MetricsCompatibilityTest.java new file mode 100644 index 0000000000..e4218eee99 --- /dev/null +++ b/micrometer-test/src/test/java/io/micrometer/binder/cache/EhCache2MetricsCompatibilityTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.UUID; + +import io.micrometer.core.lang.Nullable; +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Element; +import net.sf.ehcache.config.Configuration; +import net.sf.ehcache.config.ConfigurationFactory; +import org.junit.jupiter.api.AfterEach; + +import static java.util.Collections.emptyList; + +class EhCache2MetricsCompatibilityTest extends CacheMeterBinderCompatibilityKit { + private CacheManager cacheManager; + + @AfterEach + void after() { + cacheManager.removeAllCaches(); + } + + @Override + public void dereferenceCache() { + super.dereferenceCache(); + this.cacheManager.removeAllCaches(); + } + + @Override + public Cache createCache() { + Configuration config = ConfigurationFactory.parseConfiguration(); + config.setName(UUID.randomUUID().toString()); + + this.cacheManager = CacheManager.newInstance(config); + this.cacheManager.addCache("mycache"); + return cacheManager.getCache("mycache"); + } + + @Override + public CacheMeterBinder binder() { + return new EhCache2Metrics(cache, emptyList()); + } + + @Override + public void put(String key, String value) { + cache.put(new Element(key, value, 1)); + } + + @Nullable + @Override + public String get(String key) { + Element element = cache.get(key); + return element == null ? null : (String) element.getObjectValue(); + } +} diff --git a/micrometer-test/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsCompatibilityKit.java b/micrometer-test/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsCompatibilityKit.java new file mode 100644 index 0000000000..cca22ba0ad --- /dev/null +++ b/micrometer-test/src/test/java/io/micrometer/binder/cache/GuavaCacheMetricsCompatibilityKit.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017 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.binder.cache; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import static java.util.Collections.emptyList; + +class GuavaCacheMetricsCompatibilityKit extends CacheMeterBinderCompatibilityKit> { + private AtomicReference loadValue = new AtomicReference<>(); + + @Override + public LoadingCache createCache() { + return CacheBuilder.newBuilder() + .maximumSize(2) + .recordStats() + .build(new CacheLoader() { + @Override + public String load(String key) throws Exception { + String val = loadValue.getAndSet(null); + if (val == null) + throw new Exception("don't load this key"); + return val; + } + }); + } + + @Override + public CacheMeterBinder> binder() { + return new GuavaCacheMetrics<>(cache, "mycache", emptyList()); + } + + @Override + public void put(String key, String value) { + synchronized (this) { + loadValue.set(value); + try { + cache.get(key); + } catch (ExecutionException ignored) { + } + } + } + + @Override + public String get(String key) { + try { + return cache.get(key); + } catch (Exception ignored) { + return null; + } + } +} diff --git a/micrometer-test/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsCompatibilityTest.java b/micrometer-test/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsCompatibilityTest.java new file mode 100644 index 0000000000..a36753467c --- /dev/null +++ b/micrometer-test/src/test/java/io/micrometer/binder/cache/HazelcastCacheMetricsCompatibilityTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017 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.binder.cache; + +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.map.IMap; +import io.micrometer.core.Issue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +class HazelcastCacheMetricsCompatibilityTest extends CacheMeterBinderCompatibilityKit { + private Config config = new Config(); + private IMap cache; + + @BeforeEach + @SuppressWarnings("unchecked") + void setup() { + this.cache = (IMap) super.cache; + } + + @Override + public void dereferenceCache() { + super.dereferenceCache(); + this.cache.destroy(); + this.cache = null; + } + + @Disabled("This only demonstrates why we can't support miss count in Hazelcast.") + @Issue("#586") + @Test + void multiInstanceMissCount() { + IMap cache2 = Hazelcast.newHazelcastInstance(config).getMap("mycache"); + + // Since each member owns 1/N (N being the number of members in the cluster) entries of a distributed map, + // we add two entries so we can deterministically say that each cache will "own" one entry. + cache.put("k1", "v"); + cache.put("k2", "v"); + + cache.get("k1"); + cache.get("k2"); + + // cache stats: hits = 1, gets = 2, puts = 2 + // cache2 stats: hits = 1, gets = 0, puts = 0 + + assertThat(cache.getLocalMapStats().getHits()).isEqualTo(1); + assertThat(cache.getLocalMapStats().getGetOperationCount()).isEqualTo(2); + assertThat(cache2.getLocalMapStats().getHits()).isEqualTo(1); + + // ... and this is why we can't calculate miss count in Hazelcast. sorry! + } + + @AfterEach + void cleanup() { + Hazelcast.shutdownAll(); + } + + @Override + public IMap createCache() { + return Hazelcast.newHazelcastInstance(config).getMap("mycache"); + } + + @Override + public CacheMeterBinder binder() { + return new HazelcastCacheMetrics(super.cache, emptyList()); + } + + @Override + public void put(String key, String value) { + this.cache.put(key, value); + } + + @Override + public String get(String key) { + return this.cache.get(key); + } +} diff --git a/micrometer-test/src/test/java/io/micrometer/binder/cache/JCacheMetricsCompatibilityTest.java b/micrometer-test/src/test/java/io/micrometer/binder/cache/JCacheMetricsCompatibilityTest.java new file mode 100644 index 0000000000..79c5b5e53e --- /dev/null +++ b/micrometer-test/src/test/java/io/micrometer/binder/cache/JCacheMetricsCompatibilityTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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.binder.cache; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.expiry.AccessedExpiryPolicy; + +import org.jsr107.ri.spi.RICachingProvider; + +import static java.util.Collections.emptyList; +import static javax.cache.expiry.Duration.ONE_HOUR; + +class JCacheMetricsCompatibilityTest extends CacheMeterBinderCompatibilityKit> { + + @Override + public Cache createCache() { + CacheManager cacheManager = new RICachingProvider().getCacheManager(); + + MutableConfiguration configuration = new MutableConfiguration<>(); + configuration.setTypes(String.class, String.class) + .setExpiryPolicyFactory(AccessedExpiryPolicy.factoryOf(ONE_HOUR)) + .setStatisticsEnabled(true); + + return cacheManager.createCache("mycache", configuration); + } + + @Override + public CacheMeterBinder> binder() { + return new JCacheMetrics<>(cache, emptyList()); + } + + @Override + public void put(String key, String value) { + cache.put(key, value); + } + + @Override + public String get(String key) { + return cache.get(key); + } +} diff --git a/samples/micrometer-samples-core/build.gradle b/samples/micrometer-samples-core/build.gradle index 426fc9d569..18707c4886 100644 --- a/samples/micrometer-samples-core/build.gradle +++ b/samples/micrometer-samples-core/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation platform('io.projectreactor:reactor-bom:2020.0.+') implementation project(':micrometer-core') + implementation project(':micrometer-binders') implementation 'colt:colt' implementation('ch.qos.logback:logback-classic') implementation('org.slf4j:slf4j-api') diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/CacheSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/CacheSample.java index f3cb70a8bb..620e9de20a 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/CacheSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/CacheSample.java @@ -18,7 +18,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.cache.GuavaCacheMetrics; +import io.micrometer.binder.cache.GuavaCacheMetrics; import io.micrometer.core.samples.utils.SampleConfig; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DelimiterBasedFrameDecoder; diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/DiskMetricsSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/DiskMetricsSample.java index 5279714f56..4cc19cbd04 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/DiskMetricsSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/DiskMetricsSample.java @@ -16,7 +16,7 @@ package io.micrometer.core.samples; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.system.DiskSpaceMetrics; +import io.micrometer.binder.system.DiskSpaceMetrics; import io.micrometer.core.samples.utils.SampleConfig; import reactor.core.publisher.Flux; diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/ExecutorServiceSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/ExecutorServiceSample.java index e9a1db16bb..106d91562f 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/ExecutorServiceSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/ExecutorServiceSample.java @@ -16,7 +16,7 @@ package io.micrometer.core.samples; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import io.micrometer.binder.jvm.ExecutorServiceMetrics; import io.micrometer.core.samples.utils.SampleConfig; import reactor.core.publisher.Mono; diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/GrpcMetricsSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/GrpcMetricsSample.java index 6d10fe6449..889efca3e8 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/GrpcMetricsSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/GrpcMetricsSample.java @@ -28,8 +28,8 @@ import io.grpc.protobuf.services.HealthStatusManager; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.grpc.MetricCollectingClientInterceptor; -import io.micrometer.core.instrument.binder.grpc.MetricCollectingServerInterceptor; +import io.micrometer.binder.grpc.MetricCollectingClientInterceptor; +import io.micrometer.binder.grpc.MetricCollectingServerInterceptor; import io.micrometer.core.samples.utils.SampleConfig; /** diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/JvmMemorySample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/JvmMemorySample.java index 9468052d21..8931cf8904 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/JvmMemorySample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/JvmMemorySample.java @@ -16,7 +16,7 @@ package io.micrometer.core.samples; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; +import io.micrometer.binder.jvm.JvmMemoryMetrics; import io.micrometer.core.samples.utils.SampleConfig; import reactor.core.publisher.Flux; diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/KafkaMetricsSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/KafkaMetricsSample.java index 3b68da0e64..eb79faa2ce 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/KafkaMetricsSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/KafkaMetricsSample.java @@ -18,7 +18,7 @@ import com.github.charithe.kafka.EphemeralKafkaBroker; import com.github.charithe.kafka.KafkaHelper; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics; +import io.micrometer.binder.kafka.KafkaClientMetrics; import io.micrometer.core.samples.utils.SampleConfig; import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.KafkaProducer; diff --git a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/UptimeMetricsSample.java b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/UptimeMetricsSample.java index 9d33ba83ab..1425e6ce69 100644 --- a/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/UptimeMetricsSample.java +++ b/samples/micrometer-samples-core/src/main/java/io/micrometer/core/samples/UptimeMetricsSample.java @@ -16,7 +16,7 @@ package io.micrometer.core.samples; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.system.UptimeMetrics; +import io.micrometer.binder.system.UptimeMetrics; import io.micrometer.core.samples.utils.SampleConfig; import reactor.core.publisher.Flux; diff --git a/samples/micrometer-samples-hazelcast/build.gradle b/samples/micrometer-samples-hazelcast/build.gradle index 970e3c83a8..c6657e1ee4 100644 --- a/samples/micrometer-samples-hazelcast/build.gradle +++ b/samples/micrometer-samples-hazelcast/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { implementation project(':micrometer-core') + implementation project(':micrometer-binders') implementation 'com.hazelcast:hazelcast' implementation 'ch.qos.logback:logback-classic' } diff --git a/samples/micrometer-samples-hazelcast/src/main/java/io/micrometer/samples/hazelcast4/HazelcastCacheSample.java b/samples/micrometer-samples-hazelcast/src/main/java/io/micrometer/samples/hazelcast4/HazelcastCacheSample.java index 6c861c1f2a..3fbdc45d01 100644 --- a/samples/micrometer-samples-hazelcast/src/main/java/io/micrometer/samples/hazelcast4/HazelcastCacheSample.java +++ b/samples/micrometer-samples-hazelcast/src/main/java/io/micrometer/samples/hazelcast4/HazelcastCacheSample.java @@ -19,7 +19,7 @@ import com.hazelcast.map.IMap; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import io.micrometer.binder.cache.HazelcastCacheMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; diff --git a/samples/micrometer-samples-hazelcast3/build.gradle b/samples/micrometer-samples-hazelcast3/build.gradle index 2e567b09b3..7c4fbc86c4 100644 --- a/samples/micrometer-samples-hazelcast3/build.gradle +++ b/samples/micrometer-samples-hazelcast3/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { implementation project(':micrometer-core') + implementation project(':micrometer-binders') implementation('com.hazelcast:hazelcast') { version { strictly '3.+' diff --git a/samples/micrometer-samples-hazelcast3/src/main/java/io/micrometer/samples/hazelcast3/HazelcastCacheSample.java b/samples/micrometer-samples-hazelcast3/src/main/java/io/micrometer/samples/hazelcast3/HazelcastCacheSample.java index a88db1fa44..592f18ac8d 100644 --- a/samples/micrometer-samples-hazelcast3/src/main/java/io/micrometer/samples/hazelcast3/HazelcastCacheSample.java +++ b/samples/micrometer-samples-hazelcast3/src/main/java/io/micrometer/samples/hazelcast3/HazelcastCacheSample.java @@ -19,7 +19,7 @@ import com.hazelcast.core.IMap; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import io.micrometer.binder.cache.HazelcastCacheMetrics; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; diff --git a/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsCompatibilityTest.java b/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsCompatibilityTest.java index 6685e4c157..7a7e88aba9 100644 --- a/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsCompatibilityTest.java +++ b/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsCompatibilityTest.java @@ -18,9 +18,9 @@ import com.hazelcast.config.Config; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.IMap; -import io.micrometer.core.instrument.binder.cache.CacheMeterBinder; -import io.micrometer.core.instrument.binder.cache.CacheMeterBinderCompatibilityKit; -import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import io.micrometer.binder.cache.CacheMeterBinder; +import io.micrometer.binder.cache.CacheMeterBinderCompatibilityKit; +import io.micrometer.binder.cache.HazelcastCacheMetrics; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; diff --git a/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsTest.java b/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsTest.java index b5f5b6c4ca..45014ddfe4 100644 --- a/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsTest.java +++ b/samples/micrometer-samples-hazelcast3/src/test/java/io/micrometer/samples/hazelcast3/Hazelcast3CacheMetricsTest.java @@ -22,7 +22,7 @@ import com.hazelcast.monitor.NearCacheStats; import com.hazelcast.monitor.impl.LocalMapStatsImpl; import io.micrometer.core.instrument.*; -import io.micrometer.core.instrument.binder.cache.HazelcastCacheMetrics; +import io.micrometer.binder.cache.HazelcastCacheMetrics; import io.micrometer.core.instrument.search.RequiredSearch; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.AfterEach; diff --git a/samples/micrometer-samples-javalin/build.gradle b/samples/micrometer-samples-javalin/build.gradle index 808df4b34d..1dda0bc0a1 100644 --- a/samples/micrometer-samples-javalin/build.gradle +++ b/samples/micrometer-samples-javalin/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { implementation project(":micrometer-core") + implementation project(":micrometer-binders") implementation project(":micrometer-registry-prometheus") implementation 'io.javalin:javalin:latest.release' diff --git a/samples/micrometer-samples-javalin/src/main/java/io/micrometer/javalin/samples/PrometheusSample.java b/samples/micrometer-samples-javalin/src/main/java/io/micrometer/javalin/samples/PrometheusSample.java index 80fba2031d..8238b21be6 100644 --- a/samples/micrometer-samples-javalin/src/main/java/io/micrometer/javalin/samples/PrometheusSample.java +++ b/samples/micrometer-samples-javalin/src/main/java/io/micrometer/javalin/samples/PrometheusSample.java @@ -23,15 +23,15 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; -import io.micrometer.core.instrument.binder.http.DefaultHttpServletRequestTagsProvider; -import io.micrometer.core.instrument.binder.jetty.JettyConnectionMetrics; -import io.micrometer.core.instrument.binder.jetty.JettyServerThreadPoolMetrics; -import io.micrometer.core.instrument.binder.jetty.TimedHandler; -import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmHeapPressureMetrics; -import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics; -import io.micrometer.core.instrument.binder.system.FileDescriptorMetrics; -import io.micrometer.core.instrument.binder.system.ProcessorMetrics; +import io.micrometer.binder.http.DefaultHttpServletRequestTagsProvider; +import io.micrometer.binder.jetty.JettyConnectionMetrics; +import io.micrometer.binder.jetty.JettyServerThreadPoolMetrics; +import io.micrometer.binder.jetty.TimedHandler; +import io.micrometer.binder.jvm.JvmGcMetrics; +import io.micrometer.binder.jvm.JvmHeapPressureMetrics; +import io.micrometer.binder.jvm.JvmMemoryMetrics; +import io.micrometer.binder.system.FileDescriptorMetrics; +import io.micrometer.binder.system.ProcessorMetrics; import io.micrometer.core.instrument.util.StringUtils; import io.micrometer.core.lang.NonNull; import io.micrometer.prometheus.PrometheusConfig; diff --git a/samples/micrometer-samples-jersey3/build.gradle b/samples/micrometer-samples-jersey3/build.gradle index 53d9e2ade0..75a12e046e 100644 --- a/samples/micrometer-samples-jersey3/build.gradle +++ b/samples/micrometer-samples-jersey3/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { implementation project(":micrometer-core") + implementation project(":micrometer-binders") implementation 'org.glassfish.jersey.containers:jersey-container-jdk-http:3.+' runtimeOnly 'org.glassfish.jersey.inject:jersey-hk2:3.+' diff --git a/samples/micrometer-samples-jersey3/src/main/java/io/micrometer/samples/jersey3/Jersey3Main.java b/samples/micrometer-samples-jersey3/src/main/java/io/micrometer/samples/jersey3/Jersey3Main.java index f259fb78ed..d1ca9878ae 100644 --- a/samples/micrometer-samples-jersey3/src/main/java/io/micrometer/samples/jersey3/Jersey3Main.java +++ b/samples/micrometer-samples-jersey3/src/main/java/io/micrometer/samples/jersey3/Jersey3Main.java @@ -19,8 +19,8 @@ import com.sun.net.httpserver.HttpServer; import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; +import io.micrometer.binder.jersey.server.DefaultJerseyTagsProvider; +import io.micrometer.binder.jersey.server.MetricsApplicationEventListener; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; import jakarta.ws.rs.core.Application; diff --git a/samples/micrometer-samples-jersey3/src/test/java/io/micrometer/samples/jersey3/Jersey3Test.java b/samples/micrometer-samples-jersey3/src/test/java/io/micrometer/samples/jersey3/Jersey3Test.java index a226779bd4..3169962392 100644 --- a/samples/micrometer-samples-jersey3/src/test/java/io/micrometer/samples/jersey3/Jersey3Test.java +++ b/samples/micrometer-samples-jersey3/src/test/java/io/micrometer/samples/jersey3/Jersey3Test.java @@ -17,8 +17,8 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; +import io.micrometer.binder.jersey.server.DefaultJerseyTagsProvider; +import io.micrometer.binder.jersey.server.MetricsApplicationEventListener; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import jakarta.ws.rs.core.Application; import org.glassfish.jersey.server.ResourceConfig; diff --git a/settings.gradle b/settings.gradle index 336a1c58cc..8c2824ff16 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,7 +23,7 @@ gradleEnterprise { server = 'https://ge.micrometer.io' } -include 'micrometer-core' +include 'micrometer-core', 'micrometer-binders' ['core', 'boot2', 'boot2-reactive', 'spring-integration', 'hazelcast', 'hazelcast3', 'javalin', 'jersey3'].each { sample -> include "micrometer-samples-$sample"

    + * It monitors the overall connection pool state. + * + * @author Benjamin Hubert (benjamin.hubert@willhaben.at) + * @since 1.3.0 + */ +public class PoolingHttpClientConnectionManagerMetricsBinder implements MeterBinder { + + private final ConnPoolControl connPoolControl; + private final Iterable tags; + + /** + * Creates a metrics binder for the given pooling connection pool control. + * + * @param connPoolControl The connection pool control to monitor. + * @param name Name of the connection pool control. Will be added as tag with the + * key "httpclient". + * @param tags Tags to apply to all recorded metrics. Must be an even number + * of arguments representing key/value pairs of tags. + */ + @SuppressWarnings("WeakerAccess") + public PoolingHttpClientConnectionManagerMetricsBinder(ConnPoolControl connPoolControl, String name, String... tags) { + this(connPoolControl, name, Tags.of(tags)); + } + + /** + * Creates a metrics binder for the given connection pool control. + * + * @param connPoolControl The connection pool control to monitor. + * @param name Name of the connection pool control. Will be added as tag with the key "httpclient". + * @param tags Tags to apply to all recorded metrics. + */ + @SuppressWarnings("WeakerAccess") + public PoolingHttpClientConnectionManagerMetricsBinder(ConnPoolControl connPoolControl, String name, Iterable tags) { + this.connPoolControl = connPoolControl; + this.tags = Tags.concat(tags, "httpclient", name); + } + + @Override + public void bindTo(@NonNull MeterRegistry registry) { + registerTotalMetrics(registry); + } + + private void registerTotalMetrics(MeterRegistry registry) { + Gauge.builder("httpcomponents.httpclient.pool.total.max", + connPoolControl, + (connPoolControl) -> connPoolControl.getTotalStats().getMax()) + .description("The configured maximum number of allowed persistent connections for all routes.") + .tags(tags) + .register(registry); + Gauge.builder("httpcomponents.httpclient.pool.total.connections", + connPoolControl, + (connPoolControl) -> connPoolControl.getTotalStats().getAvailable()) + .description("The number of persistent and available connections for all routes.") + .tags(tags).tag("state", "available") + .register(registry); + Gauge.builder("httpcomponents.httpclient.pool.total.connections", + connPoolControl, + (connPoolControl) -> connPoolControl.getTotalStats().getLeased()) + .description("The number of persistent and leased connections for all routes.") + .tags(tags).tag("state", "leased") + .register(registry); + Gauge.builder("httpcomponents.httpclient.pool.total.pending", + connPoolControl, + (connPoolControl) -> connPoolControl.getTotalStats().getPending()) + .description("The number of connection requests being blocked awaiting a free connection for all routes.") + .tags(tags) + .register(registry); + Gauge.builder("httpcomponents.httpclient.pool.route.max.default", + connPoolControl, + ConnPoolControl::getDefaultMaxPerRoute) + .description("The configured default maximum number of allowed persistent connections per route.") + .tags(tags) + .register(registry); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/HystrixMetricsBinder.java b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/HystrixMetricsBinder.java new file mode 100644 index 0000000000..409e8cdf5f --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/HystrixMetricsBinder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import com.netflix.hystrix.strategy.HystrixPlugins; +import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; +import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier; +import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +@NonNullApi +@NonNullFields +public class HystrixMetricsBinder implements MeterBinder { + + @Override + public void bindTo(MeterRegistry registry) { + // Keeps references of existing Hystrix plugins. + HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier(); + HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance().getPropertiesStrategy(); + HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance().getCommandExecutionHook(); + HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy(); + HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher(); + + HystrixPlugins.reset(); + + // Registers existing plugins except the new MicroMeter Strategy plugin. + HystrixPlugins.getInstance().registerMetricsPublisher(new MicrometerMetricsPublisher(registry, metricsPublisher)); + HystrixPlugins.getInstance().registerConcurrencyStrategy(concurrencyStrategy); + HystrixPlugins.getInstance().registerEventNotifier(eventNotifier); + HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy); + HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisher.java b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisher.java new file mode 100644 index 0000000000..39dfee47af --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisher.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import com.netflix.hystrix.HystrixCircuitBreaker; +import com.netflix.hystrix.HystrixCollapserKey; +import com.netflix.hystrix.HystrixCollapserMetrics; +import com.netflix.hystrix.HystrixCollapserProperties; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixCommandMetrics; +import com.netflix.hystrix.HystrixCommandProperties; +import com.netflix.hystrix.HystrixThreadPoolKey; +import com.netflix.hystrix.HystrixThreadPoolMetrics; +import com.netflix.hystrix.HystrixThreadPoolProperties; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisherCollapser; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisherCommand; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisherThreadPool; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * @author Clint Checketts + */ +@NonNullApi +@NonNullFields +public class MicrometerMetricsPublisher extends HystrixMetricsPublisher { + private final MeterRegistry registry; + private HystrixMetricsPublisher metricsPublisher; + + public MicrometerMetricsPublisher(MeterRegistry registry, HystrixMetricsPublisher metricsPublisher) { + this.registry = registry; + this.metricsPublisher = metricsPublisher; + } + + @Override + public HystrixMetricsPublisherThreadPool getMetricsPublisherForThreadPool(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolMetrics metrics, HystrixThreadPoolProperties properties) { + final HystrixMetricsPublisherThreadPool metricsPublisherForThreadPool = + metricsPublisher.getMetricsPublisherForThreadPool(threadPoolKey, metrics, properties); + return new MicrometerMetricsPublisherThreadPool(registry, threadPoolKey, metrics, properties, metricsPublisherForThreadPool); + } + + @Override + public HystrixMetricsPublisherCollapser getMetricsPublisherForCollapser(HystrixCollapserKey collapserKey, HystrixCollapserMetrics metrics, HystrixCollapserProperties properties) { + return metricsPublisher.getMetricsPublisherForCollapser(collapserKey, metrics, properties); + } + + @Override + public HystrixMetricsPublisherCommand getMetricsPublisherForCommand(HystrixCommandKey commandKey, + HystrixCommandGroupKey commandGroupKey, + HystrixCommandMetrics metrics, + HystrixCircuitBreaker circuitBreaker, + HystrixCommandProperties properties) { + HystrixMetricsPublisherCommand metricsPublisherForCommand = + metricsPublisher.getMetricsPublisherForCommand(commandKey, commandGroupKey, metrics, circuitBreaker, properties); + return new MicrometerMetricsPublisherCommand(registry, commandKey, commandGroupKey, metrics, circuitBreaker, metricsPublisherForCommand); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommand.java b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommand.java new file mode 100644 index 0000000000..31f76da6e1 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherCommand.java @@ -0,0 +1,152 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.netflix.hystrix.HystrixCircuitBreaker; +import com.netflix.hystrix.HystrixCommandGroupKey; +import com.netflix.hystrix.HystrixCommandKey; +import com.netflix.hystrix.HystrixCommandMetrics; +import com.netflix.hystrix.HystrixEventType; +import com.netflix.hystrix.metric.HystrixCommandCompletionStream; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisherCommand; +import io.micrometer.core.instrument.Counter; +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.Timer; +import io.micrometer.core.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; +import io.micrometer.core.util.internal.logging.InternalLogger; +import io.micrometer.core.util.internal.logging.InternalLoggerFactory; + +/** + * @author Clint Checketts + */ +@NonNullApi +@NonNullFields +public class MicrometerMetricsPublisherCommand implements HystrixMetricsPublisherCommand { + private static final InternalLogger LOG = InternalLoggerFactory.getInstance(MicrometerMetricsPublisherCommand.class); + + private static final String NAME_HYSTRIX_CIRCUIT_BREAKER_OPEN = "hystrix.circuit.breaker.open"; + private static final String NAME_HYSTRIX_EXECUTION = "hystrix.execution"; + private static final String NAME_HYSTRIX_EXECUTION_TERMINAL_TOTAL = "hystrix.execution.terminal"; + private static final String NAME_HYSTRIX_LATENCY_EXECUTION = "hystrix.latency.execution"; + private static final String NAME_HYSTRIX_LATENCY_TOTAL = "hystrix.latency.total"; + private static final String NAME_HYSTRIX_CONCURRENT_EXECUTION_CURRENT = "hystrix.concurrent.execution.current"; + private static final String NAME_HYSTRIX_CONCURRENT_EXECUTION_ROLLING_MAX = "hystrix.concurrent.execution.rolling.max"; + + private static final String DESCRIPTION_HYSTRIX_EXECUTION = "Execution results. See https://github.com/Netflix/Hystrix/wiki/Metrics-and-Monitoring#command-execution-event-types-comnetflixhystrixhystrixeventtype for type definitions"; + private static final String DESCRIPTION_HYSTRIX_EXECUTION_TERMINAL_TOTAL = "Sum of all terminal executions. Use this to derive percentages from hystrix.execution"; + + private final MeterRegistry meterRegistry; + private final HystrixCommandMetrics metrics; + private final HystrixCircuitBreaker circuitBreaker; + private final Iterable tags; + private final HystrixCommandKey commandKey; + private HystrixMetricsPublisherCommand metricsPublisherForCommand; + + public MicrometerMetricsPublisherCommand(MeterRegistry meterRegistry, HystrixCommandKey commandKey, HystrixCommandGroupKey commandGroupKey, HystrixCommandMetrics metrics, HystrixCircuitBreaker circuitBreaker, HystrixMetricsPublisherCommand metricsPublisherForCommand) { + this.meterRegistry = meterRegistry; + this.metrics = metrics; + this.circuitBreaker = circuitBreaker; + this.commandKey = commandKey; + this.metricsPublisherForCommand = metricsPublisherForCommand; + + tags = Tags.of("group", commandGroupKey.name(), "key", commandKey.name()); + } + + @Override + public void initialize() { + metricsPublisherForCommand.initialize(); + Gauge.builder(NAME_HYSTRIX_CIRCUIT_BREAKER_OPEN, circuitBreaker, c -> c.isOpen() ? 1 : 0) + .tags(tags).register(meterRegistry); + + // initialize all commands counters and timers with zero + final Map eventCounters = new HashMap<>(); + Arrays.asList(HystrixEventType.values()).forEach(hystrixEventType -> { + eventCounters.put(hystrixEventType, getCounter(hystrixEventType)); + }); + + Counter terminalEventCounterTotal = Counter.builder(NAME_HYSTRIX_EXECUTION_TERMINAL_TOTAL) + .description(DESCRIPTION_HYSTRIX_EXECUTION_TERMINAL_TOTAL) + .tags(Tags.concat(tags)) + .register(meterRegistry); + + final Timer latencyExecution = Timer.builder(NAME_HYSTRIX_LATENCY_EXECUTION).tags(tags).register(meterRegistry); + final Timer latencyTotal = Timer.builder(NAME_HYSTRIX_LATENCY_TOTAL).tags(tags).register(meterRegistry); + + HystrixCommandCompletionStream.getInstance(commandKey) + .observe() + .subscribe(hystrixCommandCompletion -> { + /* + our assumptions about latency as returned by hystrixCommandCompletion: + # a latency of >= 0 indicates that this the execution occurred. + # a latency of == -1 indicates that the execution didn't occur (default in execution result) + # a latency of < -1 indicates some clock problems. + We will only count executions, and ignore non-executions with a value of -1. + Latencies of < -1 are ignored as they will decrement the counts, and Prometheus will + take this as a reset of the counter, therefore this should be avoided by all means. + */ + long totalLatency = hystrixCommandCompletion.getTotalLatency(); + if (totalLatency >= 0) { + latencyTotal.record(totalLatency, TimeUnit.MILLISECONDS); + } else if (totalLatency < -1) { + LOG.warn("received negative totalLatency, event not counted. " + + "This indicates a clock skew? {}", + hystrixCommandCompletion); + } + long executionLatency = hystrixCommandCompletion.getExecutionLatency(); + if (executionLatency >= 0) { + latencyExecution.record(executionLatency, TimeUnit.MILLISECONDS); + } else if (executionLatency < -1) { + LOG.warn("received negative executionLatency, event not counted. " + + "This indicates a clock skew? {}", + hystrixCommandCompletion); + } + for (HystrixEventType hystrixEventType : HystrixEventType.values()) { + int count = hystrixCommandCompletion.getEventCounts().getCount(hystrixEventType); + if (count > 0) { + eventCounters.get(hystrixEventType).increment(count); + if (hystrixEventType.isTerminal()) { + terminalEventCounterTotal.increment(count); + } + } + } + }); + + Gauge.builder(NAME_HYSTRIX_CONCURRENT_EXECUTION_CURRENT, metrics, HystrixCommandMetrics::getCurrentConcurrentExecutionCount) + .tags(tags) + .register(meterRegistry); + + Gauge.builder(NAME_HYSTRIX_CONCURRENT_EXECUTION_ROLLING_MAX, metrics, HystrixCommandMetrics::getRollingMaxConcurrentExecutions) + .tags(tags) + .register(meterRegistry); + } + + private Counter getCounter(HystrixEventType hystrixEventType) { + return Counter.builder(NAME_HYSTRIX_EXECUTION) + .description(DESCRIPTION_HYSTRIX_EXECUTION) + .tags(Tags.concat(tags, "event", hystrixEventType.name().toLowerCase(), "terminal", Boolean.toString(hystrixEventType.isTerminal()))) + .register(meterRegistry); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPool.java b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPool.java new file mode 100644 index 0000000000..a6b89b73ec --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/hystrix/MicrometerMetricsPublisherThreadPool.java @@ -0,0 +1,128 @@ +/* + * Copyright 2017 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.binder.hystrix; + +import com.netflix.hystrix.HystrixThreadPoolKey; +import com.netflix.hystrix.HystrixThreadPoolMetrics; +import com.netflix.hystrix.HystrixThreadPoolProperties; +import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisherThreadPool; +import io.micrometer.core.instrument.FunctionCounter; +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.lang.NonNullApi; +import io.micrometer.core.lang.NonNullFields; + +/** + * Micrometer publisher for Hystrix thread pool metrics. + * + * @since 1.2.0 + */ +@NonNullApi +@NonNullFields +public class MicrometerMetricsPublisherThreadPool implements HystrixMetricsPublisherThreadPool { + private static final String NAME_HYSTRIX_THREADPOOL = "hystrix.threadpool"; + + private final MeterRegistry meterRegistry; + private final HystrixThreadPoolMetrics metrics; + private final HystrixThreadPoolProperties properties; + private final HystrixMetricsPublisherThreadPool metricsPublisherForThreadPool; + private final Tags tags; + + public MicrometerMetricsPublisherThreadPool( + final MeterRegistry meterRegistry, + final HystrixThreadPoolKey threadPoolKey, + final HystrixThreadPoolMetrics metrics, + final HystrixThreadPoolProperties properties, + final HystrixMetricsPublisherThreadPool metricsPublisherForThreadPool) { + this.meterRegistry = meterRegistry; + this.metrics = metrics; + this.properties = properties; + this.metricsPublisherForThreadPool = metricsPublisherForThreadPool; + + this.tags = Tags.of("key", threadPoolKey.name()); + } + + @Override + public void initialize() { + metricsPublisherForThreadPool.initialize(); + + Gauge.builder(metricName("threads.active.current.count"), metrics::getCurrentActiveCount) + .description("The approximate number of threads that are actively executing tasks.") + .tags(tags) + .register(meterRegistry); + + FunctionCounter.builder(metricName("threads.cumulative.count"), metrics, HystrixThreadPoolMetrics::getCumulativeCountThreadsExecuted) + .description("Cumulative count of number of threads since the start of the application.") + .tags(tags.and(Tag.of("type", "executed"))) + .register(meterRegistry); + + FunctionCounter.builder(metricName("threads.cumulative.count"), metrics, HystrixThreadPoolMetrics::getCumulativeCountThreadsRejected) + .description("Cumulative count of number of threads since the start of the application.") + .tags(tags.and(Tag.of("type", "rejected"))) + .register(meterRegistry); + + Gauge.builder(metricName("threads.pool.current.size"), metrics::getCurrentPoolSize) + .description("The current number of threads in the pool.") + .tags(tags) + .register(meterRegistry); + + Gauge.builder(metricName("threads.largest.pool.current.size"), metrics::getCurrentLargestPoolSize) + .description("The largest number of threads that have ever simultaneously been in the pool.") + .tags(tags) + .register(meterRegistry); + + Gauge.builder(metricName("threads.max.pool.current.size"), metrics::getCurrentMaximumPoolSize) + .description("The maximum allowed number of threads.") + .tags(tags) + .register(meterRegistry); + + Gauge.builder(metricName("threads.core.pool.current.size"), metrics::getCurrentCorePoolSize) + .description("The core number of threads.") + .tags(tags) + .register(meterRegistry); + + FunctionCounter.builder(metricName("tasks.cumulative.count"), metrics, m -> m.getCurrentCompletedTaskCount().longValue()) + .description("The approximate total number of tasks since the start of the application.") + .tags(tags.and(Tag.of("type", "completed"))) + .register(meterRegistry); + + FunctionCounter.builder(metricName("tasks.cumulative.count"), metrics, m -> m.getCurrentTaskCount().longValue()) + .description("The approximate total number of tasks since the start of the application.") + .tags(tags.and(Tag.of("type", "scheduled"))) + .register(meterRegistry); + + Gauge.builder(metricName("queue.current.size"), metrics::getCurrentQueueSize) + .description("Current size of BlockingQueue used by the thread-pool.") + .tags(tags) + .register(meterRegistry); + + Gauge.builder(metricName("queue.max.size"), () -> properties.maxQueueSize().get()) + .description("Max size of BlockingQueue used by the thread-pool.") + .tags(tags) + .register(meterRegistry); + + Gauge.builder(metricName("queue.rejection.threshold.size"), () -> properties.queueSizeRejectionThreshold().get()) + .description("Artificial max size at which rejections will occur even if maxQueueSize has not been reached.") + .tags(tags) + .register(meterRegistry); + } + + private static String metricName(String name) { + return String.join(".", NAME_HYSTRIX_THREADPOOL, name); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/AnnotationFinder.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/AnnotationFinder.java new file mode 100644 index 0000000000..6041c461c4 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/AnnotationFinder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; + +public interface AnnotationFinder { + AnnotationFinder DEFAULT = new AnnotationFinder() { + }; + + /** + * The default implementation performs a simple search for a declared annotation matching the search type. + * Spring provides a more sophisticated annotation search utility that matches on meta-annotations as well. + * + * @param annotatedElement The element to search. + * @param annotationType The annotation type class. + * @param Annotation type to search for. + * @return A matching annotation. + */ + @SuppressWarnings("unchecked") + default A findAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + Annotation[] anns = annotatedElement.getDeclaredAnnotations(); + for (Annotation ann : anns) { + if (ann.annotationType() == annotationType) { + return (A) ann; + } + } + return null; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProvider.java new file mode 100644 index 0000000000..1b3bf0a327 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/DefaultJerseyTagsProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Default implementation for {@link JerseyTagsProvider}. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 1.8.0 + */ +public final class DefaultJerseyTagsProvider implements JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event), + JerseyTags.exception(event), JerseyTags.status(response), JerseyTags.outcome(response)); + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return Tags.of(JerseyTags.method(event.getContainerRequest()), JerseyTags.uri(event)); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTags.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTags.java new file mode 100644 index 0000000000..d67028b691 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTags.java @@ -0,0 +1,165 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.util.List; +import java.util.regex.Pattern; + +import io.micrometer.binder.http.Outcome; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.util.StringUtils; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ContainerResponse; +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.uri.UriTemplate; + +/** + * Factory methods for {@link Tag Tags} associated with a request-response exchange that + * is handled by Jersey server. + * + * @author Michael Weirauch + * @author Johnny Lim + * @since 1.8.0 + */ +public final class JerseyTags { + + private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); + + private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION"); + + private static final Tag URI_ROOT = Tag.of("uri", "root"); + + private static final Tag EXCEPTION_NONE = Tag.of("exception", "None"); + + private static final Tag STATUS_SERVER_ERROR = Tag.of("status", "500"); + + private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN"); + + private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$"); + + private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+"); + + private JerseyTags() { + } + + /** + * Creates a {@code method} tag based on the {@link ContainerRequest#getMethod() + * method} of the given {@code request}. + * @param request the container request + * @return the method tag whose value is a capitalized method (e.g. GET). + */ + public static Tag method(ContainerRequest request) { + return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN; + } + + /** + * Creates a {@code status} tag based on the status of the given {@code response}. + * @param response the container response + * @return the status tag derived from the status of the response + */ + public static Tag status(ContainerResponse response) { + /* In case there is no response we are dealing with an unmapped exception. */ + return (response != null) + ? Tag.of("status", Integer.toString(response.getStatus())) + : STATUS_SERVER_ERROR; + } + + /** + * Creates a {@code uri} tag based on the URI of the given {@code event}. Uses the + * {@link ExtendedUriInfo#getMatchedTemplates()} if + * available. {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND} for 404 responses. + * @param event the request event + * @return the uri tag derived from the request event + */ + public static Tag uri(RequestEvent event) { + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (isRedirection(status)) { + return URI_REDIRECTION; + } + if (status == 404 && event.getUriInfo().getMatchedResourceMethod() == null) { + return URI_NOT_FOUND; + } + } + String matchingPattern = getMatchingPattern(event); + if (matchingPattern.equals("/")) { + return URI_ROOT; + } + return Tag.of("uri", matchingPattern); + } + + private static boolean isRedirection(int status) { + return 300 <= status && status < 400; + } + + private static String getMatchingPattern(RequestEvent event) { + ExtendedUriInfo uriInfo = event.getUriInfo(); + List templates = uriInfo.getMatchedTemplates(); + + StringBuilder sb = new StringBuilder(); + sb.append(uriInfo.getBaseUri().getPath()); + for (int i = templates.size() - 1; i >= 0; i--) { + sb.append(templates.get(i).getTemplate()); + } + String multipleSlashCleaned = MULTIPLE_SLASH_PATTERN.matcher(sb.toString()).replaceAll("/"); + if (multipleSlashCleaned.equals("/")) { + return multipleSlashCleaned; + } + return TRAILING_SLASH_PATTERN.matcher(multipleSlashCleaned).replaceAll(""); + } + + /** + * Creates a {@code exception} tag based on the {@link Class#getSimpleName() simple + * name} of the class of the given {@code exception}. + * @param event the request event + * @return the exception tag derived from the exception + */ + public static Tag exception(RequestEvent event) { + Throwable exception = event.getException(); + if (exception == null) { + return EXCEPTION_NONE; + } + ContainerResponse response = event.getContainerResponse(); + if (response != null) { + int status = response.getStatus(); + if (status == 404 || isRedirection(status)) { + return EXCEPTION_NONE; + } + } + if (exception.getCause() != null) { + exception = exception.getCause(); + } + String simpleName = exception.getClass().getSimpleName(); + return Tag.of("exception", StringUtils.isNotEmpty(simpleName) ? simpleName + : exception.getClass().getName()); + } + + /** + * Creates an {@code outcome} tag based on the status of the given {@code response}. + * @param response the container response + * @return the outcome tag derived from the status of the response + */ + public static Tag outcome(ContainerResponse response) { + if (response != null) { + return Outcome.forStatus(response.getStatus()).asTag(); + } + /* In case there is no response we are dealing with an unmapped exception. */ + return Outcome.SERVER_ERROR.asTag(); + } + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTagsProvider.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTagsProvider.java new file mode 100644 index 0000000000..e07cde485b --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/JerseyTagsProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import io.micrometer.core.instrument.Tag; +import org.glassfish.jersey.server.monitoring.RequestEvent; + +/** + * Provides {@link Tag Tags} for Jersey request metrics. + * + * @author Michael Weirauch + * @since 1.8.0 + */ +public interface JerseyTagsProvider { + + /** + * Provides tags to be associated with metrics for the given {@code event}. + * + * @param event + * the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpRequestTags(RequestEvent event); + + /** + * Provides tags to be associated with the + * {@link io.micrometer.core.instrument.LongTaskTimer} which instruments the + * given long-running {@code event}. + * + * @param event + * the request event + * @return tags to associate with metrics recorded for the request + */ + Iterable httpLongRequestTags(RequestEvent event); + +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsApplicationEventListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsApplicationEventListener.java new file mode 100644 index 0000000000..a2129b3e4f --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsApplicationEventListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import io.micrometer.core.instrument.MeterRegistry; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * The Micrometer {@link ApplicationEventListener} which registers + * {@link RequestEventListener} for instrumenting Jersey server requests. + * + * @author Michael Weirauch + * @since 1.8.0 + */ +public class MetricsApplicationEventListener implements ApplicationEventListener { + + private final MeterRegistry meterRegistry; + private final JerseyTagsProvider tagsProvider; + private final String metricName; + private final AnnotationFinder annotationFinder; + private final boolean autoTimeRequests; + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, String metricName, + boolean autoTimeRequests) { + this(registry, tagsProvider, metricName, autoTimeRequests, AnnotationFinder.DEFAULT); + } + + public MetricsApplicationEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, + String metricName, boolean autoTimeRequests, + AnnotationFinder annotationFinder) { + this.meterRegistry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.annotationFinder = requireNonNull(annotationFinder); + this.autoTimeRequests = autoTimeRequests; + } + + @Override + public void onEvent(ApplicationEvent event) { + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return new MetricsRequestEventListener(meterRegistry, tagsProvider, metricName, autoTimeRequests, annotationFinder); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsRequestEventListener.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsRequestEventListener.java new file mode 100644 index 0000000000..bffa526104 --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/MetricsRequestEventListener.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.instrument.LongTaskTimer; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; + +import static java.util.Objects.requireNonNull; + +/** + * {@link RequestEventListener} recording timings for Jersey server requests. + * + * @author Michael Weirauch + * @author Jon Schneider + * @since 1.8.0 + */ +public class MetricsRequestEventListener implements RequestEventListener { + + private final Map shortTaskSample = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> longTaskSamples = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final Map> timedAnnotationsOnRequest = Collections + .synchronizedMap(new IdentityHashMap<>()); + + private final MeterRegistry registry; + private final JerseyTagsProvider tagsProvider; + private boolean autoTimeRequests; + private final TimedFinder timedFinder; + private final String metricName; + + public MetricsRequestEventListener(MeterRegistry registry, JerseyTagsProvider tagsProvider, + String metricName, boolean autoTimeRequests, AnnotationFinder annotationFinder) { + this.registry = requireNonNull(registry); + this.tagsProvider = requireNonNull(tagsProvider); + this.metricName = requireNonNull(metricName); + this.autoTimeRequests = autoTimeRequests; + this.timedFinder = new TimedFinder(annotationFinder); + } + + @Override + public void onEvent(RequestEvent event) { + ContainerRequest containerRequest = event.getContainerRequest(); + Set timedAnnotations; + + switch (event.getType()) { + case ON_EXCEPTION: + if (!isNotFoundException(event)) { + break; + } + case REQUEST_MATCHED: + timedAnnotations = annotations(event); + + timedAnnotationsOnRequest.put(containerRequest, timedAnnotations); + shortTaskSample.put(containerRequest, Timer.start(registry)); + + List longTaskSamples = longTaskTimers(timedAnnotations, event).stream().map(LongTaskTimer::start).collect(Collectors.toList()); + if (!longTaskSamples.isEmpty()) { + this.longTaskSamples.put(containerRequest, longTaskSamples); + } + break; + case FINISHED: + timedAnnotations = timedAnnotationsOnRequest.remove(containerRequest); + Timer.Sample shortSample = shortTaskSample.remove(containerRequest); + + if (shortSample != null) { + for (Timer timer : shortTimers(timedAnnotations, event)) { + shortSample.stop(timer); + } + } + + Collection longSamples = this.longTaskSamples.remove(containerRequest); + if (longSamples != null) { + for (LongTaskTimer.Sample longSample : longSamples) { + longSample.stop(); + } + } + break; + } + } + + private boolean isNotFoundException(RequestEvent event) { + Throwable t = event.getException(); + if (t == null) { + return false; + } + String className = t.getClass().getCanonicalName(); + return className.equals("jakarta.ws.rs.NotFoundException") + || className.equals("javax.ws.rs.NotFoundException"); + } + + private Set shortTimers(Set timed, RequestEvent event) { + /* + * Given we didn't find any matching resource method, 404s will be only + * recorded when auto-time-requests is enabled. On par with WebMVC + * instrumentation. + */ + if ((timed == null || timed.isEmpty()) && autoTimeRequests) { + return Collections.singleton(registry.timer(metricName, tagsProvider.httpRequestTags(event))); + } + + if (timed == null) { + return Collections.emptySet(); + } + + return timed.stream() + .filter(annotation -> !annotation.longTask()) + .map(t -> Timer.builder(t, metricName).tags(tagsProvider.httpRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set longTaskTimers(Set timed, RequestEvent event) { + return timed.stream() + .filter(Timed::longTask) + .map(LongTaskTimer::builder) + .map(b -> b.tags(tagsProvider.httpLongRequestTags(event)).register(registry)) + .collect(Collectors.toSet()); + } + + private Set annotations(RequestEvent event) { + final Set timed = new HashSet<>(); + + final ResourceMethod matchingResourceMethod = event.getUriInfo().getMatchedResourceMethod(); + if (matchingResourceMethod != null) { + // collect on method level + timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod())); + + // fallback on class level + if (timed.isEmpty()) { + timed.addAll(timedFinder.findTimedAnnotations(matchingResourceMethod.getInvocable().getHandlingMethod() + .getDeclaringClass())); + } + } + return timed; + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/TimedFinder.java b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/TimedFinder.java new file mode 100644 index 0000000000..bc53166dec --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jersey/server/TimedFinder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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.binder.jersey.server; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import io.micrometer.core.annotation.Timed; +import io.micrometer.core.annotation.TimedSet; + +class TimedFinder { + private final AnnotationFinder annotationFinder; + + TimedFinder(AnnotationFinder annotationFinder) { + this.annotationFinder = annotationFinder; + } + + Set findTimedAnnotations(AnnotatedElement element) { + Timed t = annotationFinder.findAnnotation(element, Timed.class); + if (t != null) + return Collections.singleton(t); + + TimedSet ts = annotationFinder.findAnnotation(element, TimedSet.class); + if (ts != null) { + return Arrays.stream(ts.value()).collect(Collectors.toSet()); + } + + return Collections.emptySet(); + } +} diff --git a/micrometer-binders/src/main/java/io/micrometer/binder/jetty/InstrumentedQueuedThreadPool.java b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/InstrumentedQueuedThreadPool.java new file mode 100644 index 0000000000..44dcdef9ae --- /dev/null +++ b/micrometer-binders/src/main/java/io/micrometer/binder/jetty/InstrumentedQueuedThreadPool.java @@ -0,0 +1,129 @@ +/* + * Copyright 2019 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.binder.jetty; + +import java.util.concurrent.BlockingQueue; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +/** + * A {@link QueuedThreadPool} that binds metrics about the Jetty server thread pool. + * This can be passed when constructing a Jetty server. For example: + * + *