From 19d3b611f71eac70de3eda3a389b9d25c1cb3693 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 27 Jul 2023 16:47:15 -0500 Subject: [PATCH 01/40] Initial commit of neutral metrics API with high-level interfaces; more to come --- .../io/helidon/metrics/api/MeterRegistry.java | 366 ++++++++++++++++++ .../metrics/api/MetricFactoryManager.java | 39 ++ .../java/io/helidon/metrics/api/Metrics.java | 184 +++++++++ 3 files changed, 589 insertions(+) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java new file mode 100644 index 00000000000..52baa6f96b4 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.function.ToDoubleFunction; + +/** + * Manages the look-up and registration of meters. + * + *

+ * This interface supports two types of retrieval (using {@link io.helidon.metrics.api.Counter} as an example): + *

+ *

+ *

+ * For most meter types, this interface provides two general variants of the retrieve-or-create-method for each meter + * (again using {@link io.helidon.metrics.api.Counter} as an example): + *

+ *

+ */ +public interface MeterRegistry { + + /** + * Returns all previously-registered meters. + * + * @return registered meters + */ + List meters(); + + /** + * Returns previously-registered meters which match the specified {@link java.util.function.Predicate}. + * + * @param filter the predicate with which to evaluate each {@link io.helidon.metrics.api.Meter} + * @return meters which match the predicate + */ + Iterable meters(Predicate filter); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its ID. + * + * @param id {@link Meter.Id} to register or locate + * @return new or previously-registered counter + */ + Counter counter(Meter.Id id); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its ID or registers a new one + * which wraps an external target object which provides the counter value. + * + *

+ * The counter returned rejects attempts to increment its value because the external object, not the counter itself, + * maintains the value. + *

+ * + * @param id {@link Meter.Id} to register or locate + * @param target object which provides the counter value + * @param fn function which, when applied to the target, returns the counter value + * @return the target object + * @param type of the target object + */ + Counter counter(Meter.Id id, T target, ToDoubleFunction fn); + + /** + * Locates a previous-registered {@link io.helidon.metrics.api.Counter} by its ID. + * + * @param id {@link io.helidon.metrics.api.Meter.Id} to locate + * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + */ + Optional getCounter(Meter.Id id); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter}. + * + * @param name counter name + * @param tags tags for further identifying the counter + * @return new or existing counter + */ + Counter counter(String name, Iterable tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * + * @param name counter name + * @param tags counter {@link io.helidon.metrics.api.Tag} instances which further identify the counter + * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + */ + Optional getCounter(String name, Iterable tags); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter}. + * + * @param name counter name + * @param tags key/value pairs; MUST be an even number of arguments + * @return new or existing counter + */ + Counter counter(String name, String... tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * + * @param name counter name + * @param tags counter {@link io.helidon.metrics.api.Tag} instances which further identify the counter + * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + */ + Optional getCounter(String name, String... tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name dnd tags or registers a new one + * which wraps an external target object which provides the counter value. + * + *

+ * The counter returned rejects attempts to increment its value because the external object, not the counter itself, + * maintains the value. + *

+ * + * @param name counter name + * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the counter + * @param target object which provides the counter value + * @param fn function which, when applied to the target, returns the counter value + * @return the target object + * @param type of the target object + */ + Counter counter(String name, Iterable tags, T target, ToDoubleFunction fn); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param id {@link Meter.Id} for the summary + * @param distributionStatisticsConfig configuration governing distribution statistics calculations + * @param scale scaling factor to apply to every sample recorded by the summary + * @return new or existing summary + */ + DistributionSummary summary(Meter.Id id, + DistributionStatisticsConfig distributionStatisticsConfig, + double scale); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its ID. + * + * @param id {@link io.helidon.metrics.api.Meter.Id} to locate + * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise + */ + Optional getSummary(Meter.Id id); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param name summary name + * @param tags tags for further identifying the summary + * @return new or existing distribution summary + */ + DistributionSummary summary(String name, Iterable tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. + * + * @param name summary name to locate + * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the summary + * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise + */ + Optional getSummary(String name, Iterable tags); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param name summary name + * @param tags key/value pairs; MUST be an even number of arguments + * @return new or existing distribution summary + */ + DistributionSummary summary(String name, String... tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. + * + * @param name summary name to locate + * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the summary + * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise + */ + Optional getSummary(String name, Tag... tags); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. + * + * @param id ID for the timer + * @param distributionStatisticsConfig configuration governing distribution statistics calculations + * @return new or existing timer + */ + Timer timer(Meter.Id id, DistributionStatisticsConfig distributionStatisticsConfig); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its ID. + * + * @param id ID for the timer + * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise + */ + Optional getTimer(Meter.Id id); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. + * + * @param name timer name + * @param tags tags for further identifying the timer + * @return new or existing timer + */ + Timer timer(String name, Iterable tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. + * + * @param name timer name + * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the timer + * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise + */ + Optional getTimer(String name, Iterable tags); + + /** + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. + * + * @param name timer name + * @param tags key/value pairs; MUST be an even number of arguments + * @return new or existing timer + */ + Timer timer(String name, String... tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. + * + * @param name timer name + * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the timer + * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise + */ + Optional getTimer(String name, Tag... tags); + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object returned by applying + * the specified {@code valueFunction}. + * + * @param id {@link io.helidon.metrics.api.Meter.Id} of the gauge + * @param stateObject object to which the {@code valueFunction} is applied to obtain the gauge's value + * @param fn function which, when applied to the {@code stateObject}, produces an instantaneous gauge value + * @param type of the state object which yields the gauge's value + * @return state object + */ + T gauge(Meter.Id id, + T stateObject, + ToDoubleFunction fn); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its ID. + * + * @param id ID for the gauge + * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise + */ + Optional getGauge(Meter.Id id); + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object returned by applying + * the specified {@code valueFunction}. + * + * @param name name of the gauge + * @param tags further identification of the gauge + * @param stateObject object to which the {@code valueFunction} is applied to obtain the gauge's value + * @param valueFunction function which, when applied to the {@code stateObject}, produces an instantaneous gauge value + * @param type of the state object which yields the gauge's value + * @return state object + */ + T gauge(String name, + Iterable tags, + T stateObject, + ToDoubleFunction valueFunction); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. + * + * @param name name of the gauge + * @param tags {@link Tag} instances which further identify the gauge + * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise + */ + Optional getGauge(String name, Iterable tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. + * + * @param name name of the gauge + * @param tags {@link Tag} instances which further identify the gauge + * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise + */ + Optional getGauge(String name, Tag... tags); + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the specified {@link Number} + * instance. + * + * @param name name of the gauge + * @param tags further identifies the gauge + * @param number thread-safe implementation of {@link Number} used to access the value + * @param type of the number from which the gauge value is extracted + * @return number argument passed (so the registration can be done as part of an assignment statement) + */ + T gauge(String name, Iterable tags, T number); + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the {@link Number}. + * + * @param name name of the gauge + * @param number thread-safe implementation of {@link Number} used to access the gauge's value + * @param type of the state object from which the gauge value is extracted + * @return number argument passed (so the registration can be done as part of an assignment statement) + */ + T gauge(String name, T number); + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object by applying the specified + * function. + * + * @param name name of the gauge + * @param stateObject state object used to compute a value + * @param valueFunction function which, when applied to the {@code stateObject}, yields an instantaneous gauge value + * @param type of the state object from which the gauge value is extracted + * @return state object argument passed (so the registration can be done as part + * of an assignment statement) + */ + T gauge(String name, + T stateObject, + ToDoubleFunction valueFunction); + + /** + * Removes a previously-registered meter. + * + * @param meter the meter to remove + * @return the removed meter; null if the meter is not currently registered + */ + Meter remove(Meter meter); + + /** + * Removes a previously-registered meter with the specified ID. + * + * @param id {@link Meter.Id} of the meter to remove + * @return the removed meter; null if the specified meter ID does not correspond to a registered meter + */ + Meter remove(Meter.Id id); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java new file mode 100644 index 00000000000..51014059bf6 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.ServiceLoader; + +import io.helidon.common.HelidonServiceLoader; +import io.helidon.common.LazyValue; +import io.helidon.metrics.api.spi.HelidonMetricFactory; + +/** + * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.api.spi.HelidonMetricFactory}, + * using a default no-op implementation if no other is available. + */ +class MetricFactoryManager { + + /** + * Instance of the highest-weight implementation of {@code MetricFactory} + */ + static final LazyValue INSTANCE = + LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(HelidonMetricFactory.class)) + .addService(HelidonNoOpMetricFactory.create(), Double.MIN_VALUE) + .build() + .iterator() + .next()); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java new file mode 100644 index 00000000000..0ce26762749 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.function.ToDoubleFunction; + +/** + * A main entry point to the Helidon metrics implementation, allowing access to the global meter registry and providing shortcut + * methods to register and locate meters in the global registry and remove meters from it. + */ +public interface Metrics { + + /** + * Returns the global meter registry. + * + * @return the global meter registry + */ + static MeterRegistry globalRegistry() { + return MetricFactoryManager.INSTANCE.get().globalRegistry(); + } + + /** + * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically + * increasing value. + * + * @param name counter name + * @param tags further identification of the counter + * @return new or previously-registered counter + */ + static Counter counter(String name, Iterable tags) { + return globalRegistry().counter(name, tags); + } + + /** + * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically + * increasing value. + * + * @param name counter name + * @param tags further identification of the counter; MUST be an even number of arguments representing key/value pairs + * of tags + * @return new or previously-registered counter + */ + static Counter counter(String name, String... tags) { + return globalRegistry().counter(name, tags); + } + + /** + * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically + * increasing value that is maintained by an external object, not a counter furnished by the meter registry itself. + * + *

+ * The counter returned rejects attempts to increment its value because the external object, not the counter itself, + * maintains the value. + *

+ * + * @param name counter name + * @param tags further identification of the counter + * @param target object which, when the function is applied, yields the counter value + * @param fn function which produces the counter value + * @return new or existing counter + * @param type of the object which furnishes the counter value + */ + static Counter counter(String name, Iterable tags, T target, ToDoubleFunction fn) { + return globalRegistry().counter(name, tags, target, fn); + } + + /** + * Registers a new or locates a previously-registered distribution summary, using the global registry, which measures the + * distribution of samples. + * + * @param name summary name + * @param tags further identification of the summary + * @return new or previously-registered distribution summary + */ + static DistributionSummary summary(String name, Iterable tags) { + return globalRegistry().summary(name, tags); + } + + /** + * Registers a new or locates a previously-registered distribution summary, using the global registry, which measures the + * distribution of samples. + * + * @param name summary name + * @param tags further identification of the summary; MUST be an even number of arguments representing key/value pairs + * of tags + * @return new or previously-registered distribution summary + */ + static DistributionSummary summary(String name, String... tags) { + return globalRegistry().summary(name, tags); + } + + /** + * Registers a new or locates a previously-registered timer, using the global registry, which measures the time taken for + * short tasks and the count of those tasks. + * + * @param name timer name + * @param tags further identification of the timer + * @return new or previously-registered timer + */ + static Timer timer(String name, Iterable tags) { + return globalRegistry().timer(name, tags); + } + + /** + * Registers a new or locates a previously-registered timer, using the global registry, which measures the time taken for + * short tasks and the count of those tasks. + * + * @param name timer name + * @param tags further identification of the timer; MUST be an even number of arguments representing key/value pairs of tags. + * @return new or previously-registered timer + */ + static Timer timer(String name, String... tags) { + return globalRegistry().timer(name, tags); + } + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, that reports the double value + * maintained by the specified object and exposed by the object by applying the specified function. + * + * @param name name of the gauge + * @param tags further identification of the gauge + * @param obj object which exposes the gauge value + * @param valueFunction function which, when applied to the object, yields the gauge value + * @param type of the state object which maintains the gauge's value + * @return state object + */ + static T gauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { + return globalRegistry().gauge(name, tags, obj, valueFunction); + } + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, which wraps a specific + * {@link java.lang.Number} instance. + * + * @param name name of the gauge + * @param tags further identification of the gauge + * @param number thread-safe implementation of the specified subtype of {@link java.lang.Number} which is the gauge's value + * @param specific subtype of {@code Number} which the wrapped object exposes + * @return {@code number} wrapped by this gauge + */ + static N gauge(String name, Iterable tags, N number) { + return globalRegistry().gauge(name, tags, number); + } + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, which wraps a specific + * {@link java.lang.Number} instance. + * + * @param name name of the gauge + * @param number thread-safe implementation of the specified subtype of {@link java.lang.Number} which is the gauge's value + * @param specific subtype of {@code Number} which the wrapped object exposes + * @return {@code number} wrapped by this gauge + */ + static N gauge(String name, N number) { + return globalRegistry().gauge(name, number); + } + + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, that reports the double value + * maintained by the specified object and exposed by the object by applying the specified function. + * + * @param name name of the gauge + * @param obj object which exposes the gauge value + * @param valueFunction function which, when applied to the object, yields the gauge value + * @param type of the state object which maintains the gauge's value + * @return state object + */ + static T gauge(String name, T obj, ToDoubleFunction valueFunction) { + return globalRegistry().gauge(name, obj, valueFunction); + } +} From 6eaa3163342d6f0312a580727e7e22e81ca4f64d Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 28 Jul 2023 17:18:09 -0500 Subject: [PATCH 02/40] Some clean-up of earlier push; add more API interfaces and no-op implementation --- .../java/io/helidon/metrics/api/Clock.java | 47 ++ .../io/helidon/metrics/api/CountAtBucket.java | 51 +++ .../java/io/helidon/metrics/api/Counter.java | 41 ++ .../api/DistributionStatisticsConfig.java | 204 +++++++++ .../metrics/api/DistributionSummary.java | 58 +++ .../java/io/helidon/metrics/api/Gauge.java | 34 ++ .../metrics/api/HelidonNoOpMetricFactory.java | 118 ++++++ .../metrics/api/HistogramSnapshot.java | 106 +++++ .../helidon/metrics/api/HistogramSupport.java | 29 ++ .../java/io/helidon/metrics/api/Meter.java | 113 +++++ .../io/helidon/metrics/api/MeterRegistry.java | 299 ++++++++----- .../metrics/api/MetricFactoryManager.java | 5 +- .../java/io/helidon/metrics/api/Metrics.java | 56 ++- .../io/helidon/metrics/api/NoOpMeter.java | 400 ++++++++++++++++++ .../metrics/api/NoOpMeterRegistry.java | 384 +++++++++++++++++ .../java/io/helidon/metrics/api/NoOpTag.java | 51 +++ .../main/java/io/helidon/metrics/api/Tag.java | 47 ++ .../java/io/helidon/metrics/api/Timer.java | 178 ++++++++ .../metrics/api/ValueAtPercentile.java | 47 ++ .../java/io/helidon/metrics/api/Wrapped.java | 33 ++ .../metrics/api/spi/HelidonMetricFactory.java | 235 ++++++++++ metrics/micrometer/pom.xml | 58 +++ .../micrometer/MicrometerMetricFactory.java | 61 +++ .../metrics/micrometer/MicrometerTag.java | 45 ++ .../metrics/micrometer/MicrometerTags.java | 129 ++++++ .../metrics/micrometer/package-info.java | 20 + .../micrometer/src/main/java/module-info.java | 28 ++ 27 files changed, 2739 insertions(+), 138 deletions(-) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Clock.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Counter.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Meter.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Tag.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Timer.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java create mode 100644 metrics/micrometer/pom.xml create mode 100644 metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java create mode 100644 metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java create mode 100644 metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java create mode 100644 metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java create mode 100644 metrics/micrometer/src/main/java/module-info.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java new file mode 100644 index 00000000000..9667a819d3c --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Reports absolute time (and, therefore, is also useful in computing elapsed times). + */ +public interface Clock extends Wrapped { + + /** + * Returns the current wall time in milliseconds since the epoch. + * + *

+ * Typically equivalent to System.currentTimeMillis. Should not be used to determine durations. + * For that use {@link #monotonicTime()} instead. + *

+ * + * @return wall time in milliseconds + */ + long wallTime(); + + /** + * Returns the current time in nanoseconds from a monotonic clock source. + * + *

+ * The value is only meaningful when compared with another value returned from this method to determine the elapsed time + * for an operation. The difference between two samples will have a unit of nanoseconds. The returned value is + * typically equivalent to System.nanoTime. + *

+ * + * @return monotonic time in nanoseconds + */ + long monotonicTime(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java new file mode 100644 index 00000000000..11e53ebacf9 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.concurrent.TimeUnit; + +/** + * Representation of a histogram bucket, including the bucket boundary value and the count of observations in that bucket. + *

+ * The bucket boundary value is an upper bound on the observation values that can occupy the bucket. + * That is, an observation occupies a bucket if its value is less than or equal to the bucket's boundary value. + *

+ */ +public interface CountAtBucket extends Wrapped { + + /** + * Returns the bucket boundary. + * + * @return bucket boundary value + */ + double bucket(); + + /** + * Returns the bucket boundary interpreted as a time in nanoseconds andexpressed in the specified + * {@link java.util.concurrent.TimeUnit}. + * + * @param unit time unit in which to express the bucket boundary + * @return bucket boundary value + */ + double bucket(TimeUnit unit); + + /** + * Returns the number of observations in the bucket. + * + * @return observation count for the bucket + */ + double count(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java new file mode 100644 index 00000000000..04f728a117d --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Records a monotonically increasing value. + */ +public interface Counter extends Meter { + + /** + * Updates the counter by one. + */ + void increment(); + + /** + * Updates the counter by the specified amount which should be non-negative. + * + * @param amount amount to add to the counter. + */ + void increment(double amount); + + /** + * Returns the cumulative count since this counter was registered. + * + * @return cumulative count since this counter was registered + */ + double count(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java new file mode 100644 index 00000000000..50921bcb432 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.time.Duration; + +/** + * Configuration which controls the behavior of distribution statistics from meters that support them + * (for example, timers and distribution summaries). + * + */ +public interface DistributionStatisticsConfig extends Wrapped { + + /** + * Returns whether the configuration is set for percentile histograms which can be aggregated for percentile approximations. + * + * @return whether percentile histograms are configured + */ + boolean isPercentileHistogram(); + + /** + * Returns whether the configuration is set to publish percentiles. + * + * @return true/false + */ + boolean isPublishingPercentiles(); + + /** + * Returns whether the configuration is set to publish a histogram. + * + * @return true/false + */ + boolean isPublishingHistogram(); + + /** + * Returns the settings for non-aggregable percentiles. + * + * @return percentiles to compute and publish + */ + Iterable percentiles(); + + /** + * Returns the configured number of digits of precision for percentiles. + * + * @return digits of precision to maintain for percentile approximations + */ + int percentilePrecision(); + + /** + * Returns the maximum value that the meter is expected to observe. + * + * @return maximum value that the meter is expected to observe + */ + double maximumExpectedValue(); + + /** + * Returns how long decaying past observations remain in the ring buffer. + * + * @see #bufferLength() + * @return time during which samples accumulate in a histogram + */ + Duration expiry(); + + /** + * Returns the size of the ring buffer for holding decaying observations. + * + * @return number of observations to keep in the ring buffer + */ + int bufferLength(); + + /** + * Returns the configured service level objective boundaries. + * + * @return the SLO boundaries + */ + Iterable serviceLevelObjectiveBoundaries(); + + /** + * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. + */ + interface Builder extends io.helidon.common.Builder { + + /** + * Updates the builder with non-null settings from the specified existing config. + * + * @param config the {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance to use to + * set values in this builder + * @return updated builder + */ + Builder merge(DistributionStatisticsConfig config); + + /** + * Sets how long to keep samples before they are assumed to have decayed to zero and are discareded. + * + * @param expiry how long to retain samples + * @return updated builder + */ + Builder expiry(Duration expiry); + + /** + * Sets the size of the ring buffer which holds saved samples as they decay. + * + * @param bufferLength number of histograms to keep in the ring buffer + * @return updated builder + */ + Builder bufferLength(int bufferLength); + + /** + * Sets whether to publish percentiles histograms (which are aggregable). + * + * @param enabled true to publish percentile histograms; false otherwise + * @return updated builder + */ + Builder percentilesHistogram(boolean enabled); + + /** + * Sets the minimum value that the meter is expected to observe. + * + * @param min minimum value that this distribution summary is expected to observe + * @return updated builder + */ + Builder minimumExpectedValue(double min); + + /** + * Sets the maximum value that the meter is expected to observe. + * + * @param max maximum value that the meter is expected to observe + * @return updated builder + */ + Builder maximumExpectedValue(double max); + + /** + * Specifies additional time series percentiles. + *

+ * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed + * elsewhere. In contrast, a percentile histogram triggered by invoking {@link #percentilesHistogram} can + * be aggregated. + *

+ *

+ * Specify percentiles a decimals, for example express the 95th percentile as {@code 0.95}. + *

+ * @param percentiles percentiles to compute and publish + * @return updated builder + */ + Builder percentiles(double... percentiles); + + /** + * Specifies additional time series percentiles. + *

+ * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed + * elsewhere. In contrast, a percentile histogram triggered by invoking {@link #percentilesHistogram} can + * be aggregated. + *

+ *

+ * Specify percentiles a decimals, for example express the 95th percentile as {@code 0.95}. + *

+ * @param percentiles percentiles to compute and publish + * @return updated builder + */ + Builder percentiles(Iterable percentiles); + + /** + * Sets the number of digits of precision to maintain on the dynamic range + * histogram used to compute percentile approximations. + * + * @param digitsOfPrecision digits of precision to maintain for percentile approximations + * @return updated builder + */ + Builder percentilePrecision(Integer digitsOfPrecision); + + /** + * Sets the service level objective (SLO) boundaries which, when used with + * {@link #percentilesHistogram(boolean)}, adds the boundaries defined here + * to other buckets used to generate aggregable percentile approximations. + * + * @param slos SLO boundaries + * @return updated builder + */ + Builder serviceLevelObjectives(double... slos); + + /** + * Sets the service level objective (SLO) boundaries which, when used with + * {@link #percentilesHistogram(boolean)}, adds the boundaries defined here + * to other buckets used to generate aggregable percentile approximations. + * + * @param slos SLO boundaries + * @return updated builder + */ + Builder serviceLevelObjectives(Iterable slos); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java new file mode 100644 index 00000000000..db1bf6d251b --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Records a distribution of values (e.g., sizes of responses returned by a server). + */ +public interface DistributionSummary extends Meter { + + /** + * Updates the statistics kept by the summary with the specified amount. + * + * @param amount Amount for an event being measured. For example, if the size in bytes of responses + * from a server. If the amount is less than 0 the value will be dropped. + */ + void record(double amount); + + /** + * Returns the current count of observations in the distribution summary. + * + * @return number of observations recorded in the summary + */ + long count(); + + /** + * Returns the total of the observations recorded by the distribution summary. + * + * @return total across all recorded events + */ + double totalAmount(); + + /** + * Returns the mean of the observations recorded by the distribution summary. + * + * @return average value of events recorded in the summary + */ + double mean(); + + /** + * Returns the maximum value among the observations recorded by the distribution summary. + * + * @return maximum value of recorded events + */ + double max(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java new file mode 100644 index 00000000000..07990d827b1 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Measures a value that can increase or decrease and is updated by external logic, not by explicit invocations + * of methods on this type. + */ +public interface Gauge extends Meter { + + /** + * Returns the value of the gauge. + *

+ * Invoking this method triggers the sampling of the value or invocation of the function provided when the gauge was + * registered. + *

+ * + * @return current value of the gauge + */ + double value(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java new file mode 100644 index 00000000000..ec4bd18bdce --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.function.Function; +import java.util.function.ToDoubleFunction; + +import io.helidon.metrics.api.spi.HelidonMetricFactory; + +/** + * No-op implementation of the {@link io.helidon.metrics.api.spi.MetricFactory} interface. + */ +class HelidonNoOpMetricFactory implements HelidonMetricFactory { + + private final MeterRegistry meterRegistry = null; + + static HelidonNoOpMetricFactory create() { + return new HelidonNoOpMetricFactory(); + } + + @Override + public MeterRegistry globalRegistry() { + return null; + } + + @Override + public Tag tagOf(String key, String value) { + return new NoOpTag(key, value); + } + + @Override + public Counter metricsCounter(String name, Iterable tags) { + return null; + } + + @Override + public Counter metricsCounter(String name, String... tags) { + return null; + } + + @Override + public Counter metricsCounter(String name, Iterable tags, T target, Function fn) { + return null; + } + + @Override + public DistributionSummary metricsSummary(String name, Iterable tags) { + return null; + } + + @Override + public DistributionSummary metricsSummary(String name, String... tags) { + return null; + } + + @Override + public Timer metricsTimer(String name, Iterable tags) { + return null; + } + + @Override + public Timer metricsTimer(String name, String... tags) { + return null; + } + + @Override + public T metricsGauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { + return null; + } + + @Override + public T metricsGauge(String name, Iterable tags, T number) { + return null; + } + + @Override + public T metricsGauge(String name, T number) { + return null; + } + + @Override + public T metricsGauge(String name, T obj, ToDoubleFunction valueFunction) { + return null; + } + + @Override + public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { + return null; + } + + @Override + public Timer.Sample timerStart() { + return null; + } + + @Override + public Timer.Sample timerStart(MeterRegistry registry) { + return null; + } + + @Override + public Timer.Sample timerStart(Clock clock) { + return null; + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java new file mode 100644 index 00000000000..8202c15d22f --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.io.PrintStream; +import java.util.concurrent.TimeUnit; + +/** + * Snapshot in time of a histogram. + */ +public interface HistogramSnapshot extends Wrapped { + + /** + * Returns an "empty" snapshot which has summary values but no data points. + * + * @param count count of observations the snapshot should report + * @param total total value of observations the snapshot should report + * @param max maximum value the snapshot should report + * @return empty snapshot reporting the values as specified + */ + static HistogramSnapshot empty(long count, double total, double max) { + return MetricFactoryManager.INSTANCE.get().histogramSnapshotEmpty(count, total, max); + } + + /** + * Returns the count of observations in the snapshot. + * + * @return count of observations + */ + long count(); + + /** + * Returns the total value over all observations in the snapshot. + * + * @return total value over all observations + */ + double total(); + + /** + * Returns the total value over all observations, interpreting the values as times in nanoseconds and expressing the time + * in the specified {@link java.util.concurrent.TimeUnit}. + * + * @param timeUnit time unit in which to express the total value + * @return total value expressed in the selected time unit + */ + double total(TimeUnit timeUnit); + + /** + * Returns the maximum value over all observations. + * + * @return maximum value + */ + double max(); + + /** + * Returns the average value overall observations. + * + * @return average value + */ + double mean(); + + /** + * Returns the average value over all observations, interpreting the values as times in nanoseconds and expressing the + * average in the specified {@link java.util.concurrent.TimeUnit}. + * + * @param timeUnit time unitin which to express the average + * @return average value expressed in the selected time unit + */ + double mean(TimeUnit timeUnit); + + /** + * Returns the values at the configured percentiles for the histogram. + * + * @return array of pairs of percentile and the histogram value at that percentile + */ + ValueAtPercentile[] percentileValues(); + + /** + * Returns information about each of the configured buckets for the histogram. + * + * @return array of pairs of bucket value and count of observations in that bucket + */ + CountAtBucket[] histogramCounts(); + + /** + * Dumps a summary of the snapshot to the specified {@link java.io.PrintStream} using the indicated scaling factor for + * observations. + * + * @param out {@code PrintStream} to which to dump the snapshot summary + * @param scale scale factor to apply to observations for output + */ + void outputSummary(PrintStream out, double scale); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java new file mode 100644 index 00000000000..0f9568db08e --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Common behavior among meters which support histograms. + */ +public interface HistogramSupport extends Meter { + + /** + * Returns a snapshot of the data in a histogram. + * + * @return snapshot of the histogram + */ + HistogramSnapshot takeSnapshot(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java new file mode 100644 index 00000000000..23ed1055419 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Common behavior of all meters. + */ +public interface Meter extends Wrapped { + + /** + * Unique idenfier for a meter. + */ + interface Id { + + /** + * Returns the meter name. + * + * @return meter name + */ + String name(); + + /** + * Returns the tags which further identify the meter. + * + * @return meter tags + */ + Iterable tags(); + + /** + * Unwraps the ID as the specified type. + * + * @param c {@link Class} to which to cast this ID + * @return the ID cast as the requested type + * @param type to cast to + */ + default R unwrap(Class c) { + return c.cast(this); + } + } + + /** + * Type of meter. + */ + enum Type { + + /** + * Counter (monotonically increasing value). + */ + COUNTER, + + /** + * Gauge (can increase or decrease). + */ + GAUGE, + + /** + * Timer (measures count and distribution of completed events). + */ + TIMER, + + /** + * Distribution summary (measures distribution of samples). + */ + DISTRIBUTION_SUMMARY, + + /** + * Other. + */ + OTHER; + + } + + /** + * Returns the meter ID. + * + * @return meter ID + */ + Id id(); + + /** + * Returns the meter's base unit. + * + * @return base unit + */ + String baseUnit(); + + /** + * Returns the meter's description. + * + * @return description + */ + String description(); + + /** + * Returns the meter type. + * + * @return meter type + */ + Type type(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 52baa6f96b4..859738726b8 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -26,23 +26,16 @@ *

* This interface supports two types of retrieval (using {@link io.helidon.metrics.api.Counter} as an example): *

    - *
  • retrieve or create - {@link #counter(io.helidon.metrics.api.Meter.Id)} - returns the meter if it was previously + *
  • retrieve or create - {@link #counter(String, Iterable)} - returns the meter if it was previously * registered, otherwise creates and registers the meter
  • - *
  • retrieve only - {@link #getCounter(io.helidon.metrics.api.Meter.Id)}
  • - returns an {@link java.util.Optional} - * for the meter, empty if the meter has not been registered and non-empty if it has been registered. + *
  • retrieve only - {@link #getCounter(String, Iterable)} - returns an {@link java.util.Optional} + * for the meter, empty if the meter has not been registered and non-empty if it has been registered.
  • *
- *

*

- * For most meter types, this interface provides two general variants of the retrieve-or-create-method for each meter - * (again using {@link io.helidon.metrics.api.Counter} as an example): - *

    - *
  • by ID - {@link #counter(io.helidon.metrics.api.Meter.Id)} - the caller prepares the ID
  • - *
  • by name and tags - {@link #counter(String, Iterable)} and {@link #counter(String, String...)}- the caller need not - * prepare the ID
  • - *
+ * The meter registry uniquely identifies each meter by its name and tags (if any). *

*/ -public interface MeterRegistry { +public interface MeterRegistry extends Wrapped { /** * Returns all previously-registered meters. @@ -60,76 +53,107 @@ public interface MeterRegistry { Iterable meters(Predicate filter); /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its ID. + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its + * {@link io.helidon.metrics.api.Meter.Id}. * - * @param id {@link Meter.Id} to register or locate + * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter * @return new or previously-registered counter */ Counter counter(Meter.Id id); /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its ID or registers a new one - * which wraps an external target object which provides the counter value. - * - *

- * The counter returned rejects attempts to increment its value because the external object, not the counter itself, - * maintains the value. - *

+ * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its + * {@link io.helidon.metrics.api.Meter.Id}. * - * @param id {@link Meter.Id} to register or locate - * @param target object which provides the counter value - * @param fn function which, when applied to the target, returns the counter value - * @return the target object + * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter + * @param target object which maintains the counter value + * @param fn function which, when applied to the target, yields the counter value + * @return new or previously-registered counter * @param type of the target object */ - Counter counter(Meter.Id id, T target, ToDoubleFunction fn); - + Counter counter(Meter.Id id, + T target, + ToDoubleFunction fn); /** - * Locates a previous-registered {@link io.helidon.metrics.api.Counter} by its ID. + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. * - * @param id {@link io.helidon.metrics.api.Meter.Id} to locate - * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + * @param name counter name + * @param tags tags which further identify the counter + * @return new or previously-registered counter */ - Optional getCounter(Meter.Id id); + Counter counter(String name, + Iterable tags); /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter}. + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. * * @param name counter name - * @param tags tags for further identifying the counter - * @return new or existing counter + * @param tags key/value pairs for further identifying the counter; MUST be an even number of arguments + * @return new or previously-registered counter */ - Counter counter(String name, Iterable tags); + Counter counter(String name, + String... tags); /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. * * @param name counter name - * @param tags counter {@link io.helidon.metrics.api.Tag} instances which further identify the counter - * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + * @param tags tags which further identify the counter + * @param baseUnit unit for the counter + * @param description counter description + * @return new or previously-registered counter */ - Optional getCounter(String name, Iterable tags); + Counter counter(String name, + Iterable tags, + String baseUnit, + String description); /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter}. + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags or registers a new one + * which wraps an external target object which provides the counter value. * - * @param name counter name - * @param tags key/value pairs; MUST be an even number of arguments - * @return new or existing counter + *

+ * The counter returned rejects attempts to increment its value because the external object, not the counter itself, + * maintains the value. + *

+ * + * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter + * @param baseUnit unit for the counter + * @param description counter description + * @param target object which provides the counter value + * @param fn function which, when applied to the target, returns the counter value + * @return the target object + * @param type of the target object */ - Counter counter(String name, String... tags); + Counter counter(Meter.Id id, + String baseUnit, + String description, + T target, + ToDoubleFunction fn); /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically + * increasing value that is maintained by an external object, not a counter furnished by the meter registry itself. + * + *

+ * The counter returned rejects attempts to increment its value because the external object, not the counter itself, + * maintains the value. + *

* * @param name counter name - * @param tags counter {@link io.helidon.metrics.api.Tag} instances which further identify the counter - * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + * @param tags further identification of the counter + * @param target object which, when the function is applied, yields the counter value + * @param fn function which produces the counter value + * @return new or existing counter + * @param type of the object which furnishes the counter value */ - Optional getCounter(String name, String... tags); + Counter counter(String name, + Iterable tags, + T target, + ToDoubleFunction fn); /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name dnd tags or registers a new one + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags or registers a new one * which wraps an external target object which provides the counter value. * *

@@ -138,34 +162,57 @@ public interface MeterRegistry { *

* * @param name counter name - * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the counter + * @param tags tags which further identify the counter + * @param baseUnit unit for the counter + * @param description counter description * @param target object which provides the counter value * @param fn function which, when applied to the target, returns the counter value * @return the target object * @param type of the target object */ - Counter counter(String name, Iterable tags, T target, ToDoubleFunction fn); + Counter counter(String name, + Iterable tags, + String baseUnit, + String description, + T target, + ToDoubleFunction fn); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * + * @param name counter name + * @param tags tags for further identifying the counter + * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + */ + Optional getCounter(String name, + Iterable tags); + + /** + * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * + * @param name counter name + * @param tags tags for further identifying the counter + * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise + */ + Optional getCounter(String name, + String... tags); /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. * - * @param id {@link Meter.Id} for the summary + * @param id {@link io.helidon.metrics.api.Meter.Id} for the summary + * @param baseUnit unit for the counter + * @param description counter description * @param distributionStatisticsConfig configuration governing distribution statistics calculations * @param scale scaling factor to apply to every sample recorded by the summary * @return new or existing summary */ DistributionSummary summary(Meter.Id id, + String baseUnit, + String description, DistributionStatisticsConfig distributionStatisticsConfig, double scale); - /** - * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its ID. - * - * @param id {@link io.helidon.metrics.api.Meter.Id} to locate - * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise - */ - Optional getSummary(Meter.Id id); - /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. * @@ -173,51 +220,54 @@ DistributionSummary summary(Meter.Id id, * @param tags tags for further identifying the summary * @return new or existing distribution summary */ - DistributionSummary summary(String name, Iterable tags); + DistributionSummary summary(String name, + Iterable tags); /** * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. * * @param name summary name to locate - * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the summary + * @param tags tags for further identifying the summary * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise */ - Optional getSummary(String name, Iterable tags); + Optional getSummary(String name, + Iterable tags); /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. * * @param name summary name - * @param tags key/value pairs; MUST be an even number of arguments + * @param tags key/value pairs for further identifying the summary; MUST be an even number of arguments * @return new or existing distribution summary */ - DistributionSummary summary(String name, String... tags); + DistributionSummary summary(String name, + String... tags); /** * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. * * @param name summary name to locate - * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the summary + * @param tags tags for further identifying the summary * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise */ - Optional getSummary(String name, Tag... tags); + Optional getSummary(String name, + Tag... tags); /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. * - * @param id ID for the timer + * @param name timer name + * @param tags tags for further identifying the timer + * @param baseUnit unit for the timer + * @param description timer description * @param distributionStatisticsConfig configuration governing distribution statistics calculations * @return new or existing timer */ - Timer timer(Meter.Id id, DistributionStatisticsConfig distributionStatisticsConfig); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its ID. - * - * @param id ID for the timer - * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise - */ - Optional getTimer(Meter.Id id); + Timer timer(String name, + Iterable tags, + String baseUnit, + String description, + DistributionStatisticsConfig distributionStatisticsConfig); /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. @@ -226,63 +276,47 @@ DistributionSummary summary(Meter.Id id, * @param tags tags for further identifying the timer * @return new or existing timer */ - Timer timer(String name, Iterable tags); + Timer timer(String name, + Iterable tags); /** * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. * * @param name timer name - * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the timer + * @param tags tags for further identifying the timer * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise */ - Optional getTimer(String name, Iterable tags); + Optional getTimer(String name, + Iterable tags); /** * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. * * @param name timer name - * @param tags key/value pairs; MUST be an even number of arguments + * @param tags tag key/value pairs for further identifying the timer; MUST be an even number of arguments * @return new or existing timer */ - Timer timer(String name, String... tags); + Timer timer(String name, + String... tags); /** * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. * * @param name timer name - * @param tags {@link io.helidon.metrics.api.Tag} instances which further identify the timer + * @param tags tags for further identifying the timer * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise */ - Optional getTimer(String name, Tag... tags); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object returned by applying - * the specified {@code valueFunction}. - * - * @param id {@link io.helidon.metrics.api.Meter.Id} of the gauge - * @param stateObject object to which the {@code valueFunction} is applied to obtain the gauge's value - * @param fn function which, when applied to the {@code stateObject}, produces an instantaneous gauge value - * @param type of the state object which yields the gauge's value - * @return state object - */ - T gauge(Meter.Id id, - T stateObject, - ToDoubleFunction fn); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its ID. - * - * @param id ID for the gauge - * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise - */ - Optional getGauge(Meter.Id id); + Optional getTimer(String name, + Tag... tags); /** * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object returned by applying * the specified {@code valueFunction}. * * @param name name of the gauge - * @param tags further identification of the gauge + * @param tags tags for further identifying the gauge + * @param baseUnit base unit for the gauge + * @param description gauge description * @param stateObject object to which the {@code valueFunction} is applied to obtain the gauge's value * @param valueFunction function which, when applied to the {@code stateObject}, produces an instantaneous gauge value * @param type of the state object which yields the gauge's value @@ -290,6 +324,8 @@ T gauge(Meter.Id id, */ T gauge(String name, Iterable tags, + String baseUnit, + String description, T stateObject, ToDoubleFunction valueFunction); @@ -297,31 +333,35 @@ T gauge(String name, * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. * * @param name name of the gauge - * @param tags {@link Tag} instances which further identify the gauge + * @param tags tags for further identifying the gauge * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise */ - Optional getGauge(String name, Iterable tags); + Optional getGauge(String name, + Iterable tags); /** * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. * * @param name name of the gauge - * @param tags {@link Tag} instances which further identify the gauge + * @param tags tags for further identifying the gauge * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise */ - Optional getGauge(String name, Tag... tags); + Optional getGauge(String name, + Tag... tags); /** * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the specified {@link Number} * instance. * * @param name name of the gauge - * @param tags further identifies the gauge + * @param tags tags for further identifying the gauge * @param number thread-safe implementation of {@link Number} used to access the value * @param type of the number from which the gauge value is extracted * @return number argument passed (so the registration can be done as part of an assignment statement) */ - T gauge(String name, Iterable tags, T number); + T gauge(String name, + Iterable tags, + T number); /** * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the {@link Number}. @@ -331,13 +371,15 @@ T gauge(String name, * @param type of the state object from which the gauge value is extracted * @return number argument passed (so the registration can be done as part of an assignment statement) */ - T gauge(String name, T number); + T gauge(String name, + T number); /** * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object by applying the specified * function. * * @param name name of the gauge + * @param tags tags for further identifying the gauge * @param stateObject state object used to compute a value * @param valueFunction function which, when applied to the {@code stateObject}, yields an instantaneous gauge value * @param type of the state object from which the gauge value is extracted @@ -345,9 +387,24 @@ T gauge(String name, * of an assignment statement) */ T gauge(String name, + Iterable tags, T stateObject, ToDoubleFunction valueFunction); + /** + * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object by applying the specified + * function. + * + * @param name name of the gauge + * @param stateObject state object used to compute a value + * @param valueFunction function which, when applied to the {@code stateObject}, yields an instantaneous gauge value + * @param type of the state object from which the gauge value is extracted + * @return state object argument passed (so the registration can be done as part + * of an assignment statement) + */ + T gauge(String name, + T stateObject, + ToDoubleFunction valueFunction); /** * Removes a previously-registered meter. * @@ -359,8 +416,18 @@ T gauge(String name, /** * Removes a previously-registered meter with the specified ID. * - * @param id {@link Meter.Id} of the meter to remove - * @return the removed meter; null if the specified meter ID does not correspond to a registered meter + * @param id ID for the meter to remove + * @return the removed meter; null if the meter is not currently registered */ Meter remove(Meter.Id id); + + /** + * Removes a previously-registered meter with the specified name and tags. + * + * @param name counter name + * @param tags tags for further identifying the meter + * @return the removed meter; null if the specified name and tags does not correspond to a registered meter + */ + Meter remove(String name, + Iterable tags); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java index 51014059bf6..7abab9d2fc3 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java @@ -28,7 +28,7 @@ class MetricFactoryManager { /** - * Instance of the highest-weight implementation of {@code MetricFactory} + * Instance of the highest-weight implementation of {@code MetricFactory}. */ static final LazyValue INSTANCE = LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(HelidonMetricFactory.class)) @@ -36,4 +36,7 @@ class MetricFactoryManager { .build() .iterator() .next()); + + private MetricFactoryManager() { + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 0ce26762749..390fa2aa250 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -32,18 +32,6 @@ static MeterRegistry globalRegistry() { return MetricFactoryManager.INSTANCE.get().globalRegistry(); } - /** - * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically - * increasing value. - * - * @param name counter name - * @param tags further identification of the counter - * @return new or previously-registered counter - */ - static Counter counter(String name, Iterable tags) { - return globalRegistry().counter(name, tags); - } - /** * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically * increasing value. @@ -73,7 +61,10 @@ static Counter counter(String name, String... tags) { * @return new or existing counter * @param type of the object which furnishes the counter value */ - static Counter counter(String name, Iterable tags, T target, ToDoubleFunction fn) { + static Counter counter(String name, + Iterable tags, + T target, + ToDoubleFunction fn) { return globalRegistry().counter(name, tags, target, fn); } @@ -85,7 +76,8 @@ static Counter counter(String name, Iterable tags, T target, ToDoubleFu * @param tags further identification of the summary * @return new or previously-registered distribution summary */ - static DistributionSummary summary(String name, Iterable tags) { + static DistributionSummary summary(String name, + Iterable tags) { return globalRegistry().summary(name, tags); } @@ -98,7 +90,8 @@ static DistributionSummary summary(String name, Iterable tags) { * of tags * @return new or previously-registered distribution summary */ - static DistributionSummary summary(String name, String... tags) { + static DistributionSummary summary(String name, + String... tags) { return globalRegistry().summary(name, tags); } @@ -110,7 +103,8 @@ static DistributionSummary summary(String name, String... tags) { * @param tags further identification of the timer * @return new or previously-registered timer */ - static Timer timer(String name, Iterable tags) { + static Timer timer(String name, + Iterable tags) { return globalRegistry().timer(name, tags); } @@ -122,7 +116,8 @@ static Timer timer(String name, Iterable tags) { * @param tags further identification of the timer; MUST be an even number of arguments representing key/value pairs of tags. * @return new or previously-registered timer */ - static Timer timer(String name, String... tags) { + static Timer timer(String name, + String... tags) { return globalRegistry().timer(name, tags); } @@ -137,7 +132,10 @@ static Timer timer(String name, String... tags) { * @param type of the state object which maintains the gauge's value * @return state object */ - static T gauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { + static T gauge(String name, + Iterable tags, + T obj, + ToDoubleFunction valueFunction) { return globalRegistry().gauge(name, tags, obj, valueFunction); } @@ -151,7 +149,9 @@ static T gauge(String name, Iterable tags, T obj, ToDoubleFunction v * @param specific subtype of {@code Number} which the wrapped object exposes * @return {@code number} wrapped by this gauge */ - static N gauge(String name, Iterable tags, N number) { + static N gauge(String name, + Iterable tags, + N number) { return globalRegistry().gauge(name, tags, number); } @@ -164,7 +164,8 @@ static N gauge(String name, Iterable tags, N number) { * @param specific subtype of {@code Number} which the wrapped object exposes * @return {@code number} wrapped by this gauge */ - static N gauge(String name, N number) { + static N gauge(String name, + N number) { return globalRegistry().gauge(name, number); } @@ -178,7 +179,20 @@ static N gauge(String name, N number) { * @param type of the state object which maintains the gauge's value * @return state object */ - static T gauge(String name, T obj, ToDoubleFunction valueFunction) { + static T gauge(String name, + T obj, + ToDoubleFunction valueFunction) { return globalRegistry().gauge(name, obj, valueFunction); } + + /** + * Creates a {@link Tag} for the specified key and value. + * + * @param key tag key + * @param value tag value + * @return new tag + */ + static Tag tag(String key, String value) { + return MetricFactoryManager.INSTANCE.get().tagOf(key, value); + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java new file mode 100644 index 00000000000..0bacc75f7c5 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + +class NoOpMeter implements Meter { + + private final Id id; + private final String unit; + private final String description; + private final Type type; + + static class Id implements Meter.Id { + + static Id create(String name, Iterable tags) { + return new Id(name, tags); + } + + static Id create(String name, Tag... tags) { + return new Id(name, Arrays.asList(tags)); + } + + private final String name; + private final List tags = new ArrayList<>(); // must be ordered by tag name for consistency + + private Id(String name, Iterable tags) { + this.name = name; + tags.forEach(this.tags::add); + this.tags.sort(Comparator.comparing(Tag::key)); + } + + @Override + public String name() { + return name; + } + + @Override + public List tags() { + return tags.stream().toList(); + } + + Iterable tagsAsIterable() { + return tags; + } + + String tag(String key) { + return tags.stream() + .filter(t -> t.key().equals(key)) + .map(Tag::value) + .findFirst() + .orElse(null); + } + } + + private NoOpMeter(NoOpMeter.Builder builder) { + this(new NoOpMeter.Id(builder.name, builder.tags.values()), + builder.unit, + builder.description, + builder.type); + } + + private NoOpMeter(Id id, String baseUnit, String description, Type type) { + this.id = id; + this.unit = baseUnit; + this.description = description; + this.type = type; + } + + @Override + public Id id() { + return id; + } + + @Override + public String baseUnit() { + return unit; + } + + @Override + public String description() { + return description; + } + + @Override + public Type type() { + return type; + } + + abstract static class Builder, M extends Meter> implements io.helidon.common.Builder { + + private final String name; + private final Map tags = new TreeMap<>(); // tree map for ordering by tag name + private String description; + private String unit; + private final Type type; + + private Builder(String name, Type type) { + this.name = name; + this.type = type; + } + + public abstract M build(); + + B tags(String... tags) { + if (tags.length % 2 != 0) { + throw new IllegalArgumentException(""" + Tag list must contain an even number of items because they must \ + be key/value pairs")"""); + } + for (int slot = 0; slot < tags.length / 2; slot++) { + this.tags.put(tags[slot * 2], Tag.of(tags[slot * 2], tags[slot * 2 + 1])); + } + return identity(); + } + + B tags(Iterable tags) { + tags.forEach(tag -> this.tags.put(tag.key(), tag)); + return identity(); + } + + B tag(String key, String value) { + tags.put(key, Tag.of(key, value)); + return identity(); + } + + B description(String description) { + this.description = description; + return identity(); + } + + B baseUnit(String unit) { + this.unit = unit; + return identity(); + } + } + + static class Counter extends NoOpMeter implements io.helidon.metrics.api.Counter { + + static Counter create(String name, Iterable tags) { + return builder(name) + .tags(tags) + .build(); + } + + static class Builder extends NoOpMeter.Builder { + + protected Builder(String name) { + super(name, Type.COUNTER); + } + + + @Override + public Counter build() { + return new NoOpMeter.Counter(this); + } + + } + + static Counter.Builder builder(String name) { + return new Builder(name); + } + + static FunctionalCounter.Builder builder(String name, T target, ToDoubleFunction fn) { + return new FunctionalCounter.Builder<>(name, target, fn); + } + + protected Counter(Builder builder) { + super(builder); + } + + @Override + public void increment() { + } + + @Override + public void increment(double amount) { + } + + @Override + public double count() { + return 0; + } + } + + static class FunctionalCounter extends Counter { + + static class Builder extends Counter.Builder { + + private Builder(String name, T target, ToDoubleFunction fn) { + super(name); + } + + @Override + public FunctionalCounter build() { + return new FunctionalCounter<>(this); + } + } + + private FunctionalCounter(Builder builder) { + super(builder); + } + + @Override + public void increment() { + throw new UnsupportedOperationException(); + } + + @Override + public void increment(double amount) { + throw new UnsupportedOperationException(); + } + } + + static class DistributionSummary extends NoOpMeter implements io.helidon.metrics.api.DistributionSummary { + + static class Builder extends NoOpMeter.Builder { + + private Builder(String name) { + super(name, Type.DISTRIBUTION_SUMMARY); + } + + @Override + public DistributionSummary build() { + return new DistributionSummary(this); + } + } + + static DistributionSummary.Builder builder(String name) { + return new DistributionSummary.Builder(name); + } + + private DistributionSummary(Builder builder) { + super(builder); + } + + @Override + public void record(double amount) { + } + + @Override + public long count() { + return 0; + } + + @Override + public double totalAmount() { + return 0; + } + + @Override + public double mean() { + return 0; + } + + @Override + public double max() { + return 0; + } + } + + static class Gauge extends NoOpMeter implements io.helidon.metrics.api.Gauge { + + static Builder builder(String name) { + return new Builder(name); + } + + static class Builder extends NoOpMeter.Builder { + + private Builder(String name) { + super(name, Type.GAUGE); + } + + @Override + public Gauge build() { + return new Gauge(this); + } + } + + private Gauge(Builder builder) { + super(builder); + } + + @Override + public double value() { + return 0; + } + } + + static class Timer extends NoOpMeter implements io.helidon.metrics.api.Timer { + + static class Builder extends NoOpMeter.Builder { + + private Builder(String name) { + super(name, Type.TIMER); + } + + @Override + public Timer build() { + return new Timer(this); + } + } + + static Builder builder(String name) { + return new Builder(name); + } + + private Timer(Builder builder) { + super(builder); + } + + @Override + public HistogramSnapshot takeSnapshot() { + return null; + } + + @Override + public void record(long amount, TimeUnit unit) { + + } + + @Override + public void record(Duration duration) { + + } + + @Override + public T record(Supplier f) { + return null; + } + + @Override + public T recordCallable(Callable f) throws Exception { + return null; + } + + @Override + public void record(Runnable f) { + + } + + @Override + public Runnable wrap(Runnable f) { + return null; + } + + @Override + public Callable wrap(Callable f) { + return null; + } + + @Override + public Supplier wrap(Supplier f) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public double totalTime(TimeUnit unit) { + return 0; + } + + @Override + public double mean(TimeUnit unit) { + return 0; + } + + @Override + public double max(TimeUnit unit) { + return 0; + } + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java new file mode 100644 index 00000000000..849b8efb88a --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; + +/** + * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}. + */ +class NoOpMeterRegistry implements MeterRegistry { + + private final Map meters = new ConcurrentHashMap<>(); + + private final ReentrantLock metersAccess = new ReentrantLock(); + + @Override + public List meters() { + return List.of(meters.values().toArray(new Meter[0])); + } + + @Override + public Iterable meters(Predicate filter) { + return () -> new Iterator<>() { + + private final Iterator iter = meters.values().iterator(); + private Meter nextMatch = nextMatch(); + + private Meter nextMatch() { + while (iter.hasNext()) { + Meter candidate = iter.next(); + if (filter.test(candidate)) { + return candidate; + } + } + return null; + } + + @Override + public boolean hasNext() { + return nextMatch != null; + } + + @Override + public Meter next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Meter result = nextMatch; + nextMatch = nextMatch(); + return result; + } + }; + } + + @Override + public Meter remove(Meter.Id id) { + return meters.remove(id); + } + + @Override + public Meter remove(Meter meter) { + return meters.remove(meter.id()); + } + + @Override + public Meter remove(String name, Iterable tags) { + return remove(NoOpMeter.Id.create(name, tags)); + } + + @Override + public Counter counter(Meter.Id id) { + return findOrRegister(id, + Counter.class, + () -> NoOpMeter.Counter.builder(id.name()) + .tags(id.tags()) + .build()); + } + + @Override + public Counter counter(Meter.Id id, + T target, + ToDoubleFunction fn) { + return findOrRegister(id, + Counter.class, + () -> NoOpMeter.FunctionalCounter.builder(id.name(), target, fn) + .tags(id.tags()) + .build()); + } + + @Override + public Counter counter(String name, + Iterable tags, + String baseUnit, + String description, + T target, + ToDoubleFunction fn) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + Counter.class, + () -> NoOpMeter.FunctionalCounter.builder(name, target, fn) + .tags(tags) + .baseUnit(baseUnit) + .description(description) + .build()); + } + + @Override + public Counter counter(Meter.Id id, + String baseUnit, + String description, + T target, + ToDoubleFunction fn) { + return findOrRegister(id, + Counter.class, + () -> NoOpMeter.FunctionalCounter.builder(id.name(), target, fn) + .tags(id.tags()) + .baseUnit(baseUnit) + .description(description) + .build()); + } + + @Override + public Optional getCounter(String name, Iterable tags) { + return find(NoOpMeter.Id.create(name, tags), Counter.class); + } + + + @Override + public Counter counter(String name, Iterable tags) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + Counter.class, + () -> NoOpMeter.Counter.builder(name) + .tags(tags) + .build()); + } + + @Override + public Counter counter(String name, String... tags) { + Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); + return findOrRegister(id, + Counter.class, + () -> NoOpMeter.Counter.builder(name) + .tags(id.tags()) + .build()); + } + + @Override + public Counter counter(String name, + Iterable tags, + String baseUnit, + String description) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + Counter.class, + () -> NoOpMeter.Counter.builder(name) + .tags(tags) + .baseUnit(baseUnit) + .description(description) + .build()); + } + + @Override + public Counter counter(String name, + Iterable tags, + T target, + ToDoubleFunction fn) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + Counter.class, + () -> NoOpMeter.Counter.builder(name, target, fn) + .tags(tags) + .build()); + } + + @Override + public Optional getCounter(String name, String... tags) { + return find(NoOpMeter.Id.create(name, NoOpTag.tags(tags)), + Counter.class); + } + + + + @Override + public DistributionSummary summary(String name, Iterable tags) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + DistributionSummary.class, + () -> NoOpMeter.DistributionSummary.builder(name) + .tags(tags) + .build()); + } + + @Override + public DistributionSummary summary(Meter.Id id, + String baseUnit, + String description, + DistributionStatisticsConfig distributionStatisticsConfig, + double scale) { + return findOrRegister(id, + DistributionSummary.class, + () -> NoOpMeter.DistributionSummary.builder(id.name()) + .baseUnit(baseUnit) + .description(description) + .build()); + } + + @Override + public Optional getSummary(String name, Iterable tags) { + return find(NoOpMeter.Id.create(name, tags), + DistributionSummary.class); + } + + @Override + public Optional getSummary(String name, Tag... tags) { + return find(NoOpMeter.Id.create(name, tags), + DistributionSummary.class); + } + + + + @Override + public T gauge(String name, T stateObject, ToDoubleFunction valueFunction) { + // We don't need a variant of the builder to handle the state object and function because the no-op gauges + // don't need to operate anyway. + findOrRegister(NoOpMeter.Id.create(name, Set.of()), + Gauge.class, + () -> NoOpMeter.Gauge.builder(name) + .build()); + return stateObject; + } + + @Override + public T gauge(String name, Iterable tags, T stateObject, ToDoubleFunction valueFunction) { + findOrRegister(NoOpMeter.Id.create(name, tags), + Gauge.class, + () -> NoOpMeter.Gauge.builder(name) + .build()); + return stateObject; + } + + @Override + public T gauge(String name, Iterable tags, T number) { + findOrRegister(NoOpMeter.Id.create(name, tags), + Gauge.class, + () -> NoOpMeter.Gauge.builder(name) + .build()); + return number; + } + + @Override + public T gauge(String name, T number) { + findOrRegister(NoOpMeter.Id.create(name, Set.of()), + Gauge.class, + () -> NoOpMeter.Gauge.builder(name) + .build()); + return number; + } + + @Override + public T gauge(String name, + Iterable tags, + String baseUnit, + String description, + T stateObject, + ToDoubleFunction valueFunction) { + findOrRegister(NoOpMeter.Id.create(name, tags), + Gauge.class, + () -> NoOpMeter.Gauge.builder(name) + .baseUnit(baseUnit) + .description(description) + .build()); + return stateObject; + } + + @Override + public Optional getGauge(String name, Iterable tags) { + return find(NoOpMeter.Id.create(name, tags), + Gauge.class); + } + + @Override + public Optional getGauge(String name, Tag... tags) { + return find(NoOpMeter.Id.create(name, tags), + Gauge.class); + } + + @Override + public DistributionSummary summary(String name, String... tags) { + Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); + return findOrRegister(id, + DistributionSummary.class, + () -> NoOpMeter.DistributionSummary.builder(name) + .tags(id.tags()) + .build()); + } + + @Override + public Timer timer(String name, Iterable tags) { + return findOrRegister(NoOpMeter.Id.create(name, tags), + Timer.class, + () -> NoOpMeter.Timer.builder(name) + .tags(tags) + .build()); + } + + @Override + public Timer timer(String name, String... tags) { + Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); + return findOrRegister(id, + Timer.class, + () -> NoOpMeter.Timer.builder(name) + .tags(id.tags()) + .build()); + } + + @Override + public Timer timer(String name, + Iterable tags, + String baseUnit, + String description, + DistributionStatisticsConfig distributionStatisticsConfig) { + // The distribution is also a no-op, so we ignore the statistics config. + return findOrRegister(NoOpMeter.Id.create(name, tags), + Timer.class, + () -> NoOpMeter.Timer.builder(name) + .baseUnit(baseUnit) + .description(description) + .build()); + } + + @Override + public Optional getTimer(String name, Tag... tags) { + return find(NoOpMeter.Id.create(name, tags), + Timer.class); + } + + @Override + public Optional getTimer(String name, Iterable tags) { + return find(NoOpMeter.Id.create(name, tags), + Timer.class); + } + + private Optional find(Meter.Id id, Class mClass) { + return Optional.ofNullable(mClass.cast(meters.get(id))); + } + + private M findOrRegister(Meter.Id id, Class mClass, Supplier meterSupplier) { + // This next step is atomic because we are using a ConcurrentHashMap. + Meter result = meters.computeIfAbsent(id, + theId -> meterSupplier.get()); + + // Check the type in case we retrieved a previously-registered meter with the specified ID. The type will always + // be correct if we ran the supplier, in which this test is unneeded by mostly harmless. + // We could just attempt the cast and let Java throw a class cast exception itself if needed, but this is nicer. + if (!mClass.isInstance(result)) { + throw new IllegalArgumentException( + String.format("Found previously-registered meter with ID %s of type %s when expecting %s", + id, + result.getClass().getName(), + mClass.getName())); + } + + return mClass.cast(meters.put(id, meterSupplier.get())); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java new file mode 100644 index 00000000000..933124f1f9c --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +record NoOpTag(String key, String value) implements Tag { + + static Tag of(String key, String value) { + return new NoOpTag(key, value); + } + + static Iterable tags(String... keysAndValues) { + if (keysAndValues.length % 2 != 0) { + throw new IllegalArgumentException("String array of keys and values must balance (have an even length"); + } + return () -> new Iterator<>() { + + private int slot; + + @Override + public boolean hasNext() { + return slot < keysAndValues.length / 2; + } + + @Override + public Tag next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Tag result = Tag.of(keysAndValues[2 * slot], keysAndValues[2 * slot + 1]); + slot++; + return result; + } + }; + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java new file mode 100644 index 00000000000..42519f34d9b --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Behavior of a tag for further identifying meters. + */ +public interface Tag extends Wrapped { + + /** + * Returns the tag's key. + * + * @return the tag's key + */ + String key(); + + /** + * Returns the tag's value. + * + * @return the tag's value + */ + String value(); + + /** + * Creates a new tag using the specified key and value. + * + * @param key the tag's key + * @param value the tag's value + * @return new {@code Tag} representing the key and value + */ + static Tag of(String key, String value) { + return MetricFactoryManager.INSTANCE.get().tagOf(key, value); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java new file mode 100644 index 00000000000..9b2c947a718 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +/** + * Records timing information about large numbers of short-running events (e.g., HTTP requests). + */ +public interface Timer extends Meter, HistogramSupport { + + /** + * Starts a timing sample using the default system clock. + * + * @return new sample + */ + static Sample start() { + return MetricFactoryManager.INSTANCE.get().timerStart(); + } + + /** + * Starts a timing sample using the clock associated with the specified {@link io.helidon.metrics.api.MeterRegistry}. + * + * @param registry the meter registry whose clock is to be used for measuring the interval + * @return new sample with start time recorded + */ + static Sample start(MeterRegistry registry) { + return MetricFactoryManager.INSTANCE.get().timerStart(registry); + } + + /** + * Starts a timing sample using the specified clock. + * + * @param clock a clock to be used + * @return new sample with start time recorded + */ + static Sample start(Clock clock) { + return MetricFactoryManager.INSTANCE.get().timerStart(clock); + } + + /** + * Updates the statistics kept by the timer with the specified amount. + * + * @param amount duration of a single event being measured by this timer. If the amount is less than 0 + * the value will be dropped + * @param unit time unit for the amount being recorded + */ + void record(long amount, TimeUnit unit); + + /** + * Updates the statistics kept by the timer with the specified amount. + * + * @param duration duration of a single event being measured by this timer + */ + void record(Duration duration); + + /** + * Executes the {@link java.util.function.Supplier} {@code f} and records the time spent invoking the function. + * + * @param f function to be timed + * @param return type of the {@link java.util.function.Supplier} + * @return return value from invoking the function {@code f} + */ + T record(Supplier f); + + /** + * Executes the {@link java.util.concurrent.Callable} {@code f} and records the time spent it, returning the + * callable's result. + * + * @param f callable to be timed + * @param return type of the {@link java.util.concurrent.Callable} + * @return return value from invoking the callable {@code f} + * @throws Exception exception escaping from the callable + */ + T recordCallable(Callable f) throws Exception; + + /** + * Executes the {@link java.lang.Runnable} {@code f} and records the time it takes. + * + * @param f runnable to be timed + */ + void record(Runnable f); + + /** + * Wraps a {@link Runnable} so that it will be timed every time it is invoked via the return value from this method. + * + * @param f runnable to time when it is invoked + * @return the wrapped runnable + */ + Runnable wrap(Runnable f); + + /** + * Wraps a {@link Callable} so that it is will be timed every time it is invoked via the return value from this method. + * + * @param f callable to time when it is invoked + * @param return type of the callable + * @return the wrapped callable + */ + Callable wrap(Callable f); + + /** + * Wraps a {@link Supplier} so that it will be timed every time it is invoked via the return value from this method. + * + * @param f {@code Supplier} to time when it is invoked + * @param return type of the {@code Supplier} result + * @return the wrapped supplier + */ + Supplier wrap(Supplier f); + + /** + * Returns the current count of completed events measured by the timer. + * + * @return number of events recorded by the timer + */ + long count(); + + /** + * Returns the total time, expressed in the specified units, consumed by completed events + * measured by the timer. + * + * @param unit time unit in which to express the total accumulated time + * @return total time of recorded events + */ + double totalTime(TimeUnit unit); + + /** + * Returns the average time, expressed in the specified units, consumed by completed events + * measured by the timer. + * + * @param unit time unit in which to express the mean + * @return average for all events recorded by this timer + */ + double mean(TimeUnit unit); + + /** + * Returns the maximum value, expressed in the specified units, consumed by a completed event + * measured by the timer. + * + * @param unit time unit in which to express the maximum + * @return maximum time recorded by a single event measured by this timer + */ + double max(TimeUnit unit); + + /** + * Measures an interval of time from instantiation to an explicit invocation of {@link #stop(io.helidon.metrics.api.Timer)}. + *

+ * A {@code Sample} is not bound to a specific {@code Timer} until it is stopped, at + * which time the caller specifies which timer to update. + *

+ */ + interface Sample { + + /** + * Ends the interval, recording the current time as the end time of the interval and applying the elapsed time to the + * specified {@link io.helidon.metrics.api.Timer}. + * + * @param timer the timer to update with this interval + * @return duration of the sample (in nanoseconds) + */ + long stop(Timer timer); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java b/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java new file mode 100644 index 00000000000..7e2b3881142 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.concurrent.TimeUnit; + +/** + * Percentile and value at that percentile within a distribution. + */ +public interface ValueAtPercentile extends Wrapped { + + /** + * Returns the percentile. + * + * @return the percentile + */ + double percentile(); + + /** + * Returns the value at this percentile. + * + * @return the percentile's value + */ + double value(); + + /** + * Returns the value of this percentile interpreted as time in nanoseconds converted to the specified + * {@link java.util.concurrent.TimeUnit}. + * + * @param unit time unit in which to express the value + * @return converted value + */ + double value(TimeUnit unit); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java new file mode 100644 index 00000000000..4abcd478895 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +/** + * Behavior of a type that wraps a related type. + */ +public interface Wrapped { + + /** + * Unwraps the meter registry as the specified type. + * + * @param c {@link Class} to which to cast this meter registry + * @return the meter registry cast as the requested type + * @param type to cast to + */ + default R unwrap(Class c) { + return c.cast(this); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java new file mode 100644 index 00000000000..c50a902f7d0 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api.spi; + +import java.util.function.Function; +import java.util.function.ToDoubleFunction; + +import io.helidon.metrics.api.Clock; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.DistributionSummary; +import io.helidon.metrics.api.HistogramSnapshot; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +/** + * Factory for creating implementation instances of the Helidon metrics API. + *

+ * An implementation of this interface provides instance methods for each + * of the static methods on the Helidon metrics API interfaces. The prefix of each method + * here identifies the interface that bears the corresponding static method. For example, + * {@link #timerStart(io.helidon.metrics.api.MeterRegistry)} corresponds to the static + * {@link Timer#start(io.helidon.metrics.api.MeterRegistry)} method. + *

+ */ +public interface HelidonMetricFactory { + + /** + * Returns the global meter registry. + * + * @return the global meter registry + */ + MeterRegistry globalRegistry(); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using + * the system default {@link io.helidon.metrics.api.Clock}. + * + * @return new sample + */ + Timer.Sample timerStart(); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration, using the + * clock associated with the specified {@link io.helidon.metrics.api.MeterRegistry}. + * + * @param registry the meter registry whose {@link io.helidon.metrics.api.Clock} is to be used + * @return new sample with the start time recorded + */ + Timer.Sample timerStart(MeterRegistry registry); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using + * the specified {@link io.helidon.metrics.api.Clock}. + * + * @param clock the clock to use for measuring the duration + * @return new sample + */ + Timer.Sample timerStart(Clock clock); + + /** + * Creates a {@link io.helidon.metrics.api.Tag} from the specified key and value. + * + * @param key tag key + * @param value tag value + * @return new {@code Tag} instance + */ + Tag tagOf(String key, String value); + + /** + * Tracks a monotonically increasing value. + * + * @param name The base metric name + * @param tags Sequence of dimensions for breaking down the name. + * @return A new or existing counter. + */ + default Counter metricsCounter(String name, Iterable tags) { + return globalRegistry().counter(name, tags); + } + + /** + * Tracks a monotonically increasing value. + * + * @param name The base metric name + * @param tags MUST be an even number of arguments representing key/value pairs of tags. + * @return A new or existing counter. + */ + default Counter metricsCounter(String name, String... tags) { + return globalRegistry().counter(name, tags); + } + + /** + * Tracks a monotonically increasing value as maintained by an external variable. + * + * @param name meter name + * @param tags tags for further identifying the meter + * @param target object which, when the function is applied, provides the counter value + * @param fn function which obtains the counter value from the target object + * @return counter + * @param type of the target object + */ + Counter metricsCounter(String name, Iterable tags, T target, Function fn); + + /** + * Measures the distribution of samples. + * + * @param name The base metric name + * @param tags Sequence of dimensions for breaking down the name. + * @return A new or existing distribution summary. + */ + default DistributionSummary metricsSummary(String name, Iterable tags) { + return globalRegistry().summary(name, tags); + } + + /** + * Creates a new {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param name name of the new meter + * @param tags tags for identifying the new meter + * @return new {@code DistributionSummary} + */ + default DistributionSummary metricsSummary(String name, String... tags) { + return globalRegistry().summary(name, tags); + } + + /** + * Measures the time taken for short tasks and the count of these tasks. + * + * @param name The base metric name + * @param tags Sequence of dimensions for breaking down the name. + * @return A new or existing timer. + */ + default Timer metricsTimer(String name, Iterable tags) { + return globalRegistry().timer(name, tags); + } + + /** + * Measures the time taken for short tasks and the count of these tasks. + * + * @param name The base metric name + * @param tags MUST be an even number of arguments representing key/value pairs of tags. + * @return A new or existing timer. + */ + default Timer metricsTimer(String name, String... tags) { + return globalRegistry().timer(name, tags); + } + + /** + * Register a gauge that reports the value of the object after the function + * {@code valueFunction} is applied. The registration will keep a weak reference to the object so it will + * not prevent garbage collection. Applying {@code valueFunction} on the object should be thread safe. + *

+ * If multiple gauges are registered with the same id, then the values will be aggregated and + * the sum will be reported. For example, registering multiple gauges for active threads in + * a thread pool with the same id would produce a value that is the overall number + * of active threads. For other behaviors, manage it on the user side and avoid multiple + * registrations. + * + * @param name Name of the gauge being registered. + * @param tags Sequence of dimensions for breaking down the name. + * @param obj Object used to compute a value. + * @param valueFunction Function that is applied on the value for the number. + * @param The type of the state object from which the gauge value is extracted. + * @return The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + default T metricsGauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { + return globalRegistry().gauge(name, tags, obj, valueFunction); + } + + /** + * Register a gauge that reports the value of the {@link java.lang.Number}. + * + * @param name Name of the gauge being registered. + * @param tags Sequence of dimensions for breaking down the name. + * @param number Thread-safe implementation of {@link Number} used to access the value. + * @param The type of the state object from which the gauge value is extracted. + * @return The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + default T metricsGauge(String name, Iterable tags, T number) { + return globalRegistry().gauge(name, tags, number); + } + + /** + * Register a gauge that reports the value of the {@link java.lang.Number}. + * + * @param name Name of the gauge being registered. + * @param number Thread-safe implementation of {@link Number} used to access the value. + * @param The type of the state object from which the gauge value is extracted. + * @return The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + default T metricsGauge(String name, T number) { + return globalRegistry().gauge(name, number); + } + + /** + * Register a gauge that reports the value of the object. + * + * @param name Name of the gauge being registered. + * @param obj Object used to compute a value. + * @param valueFunction Function that is applied on the value for the number. + * @param The type of the state object from which the gauge value is extracted.F + * @return The number that was passed in so the registration can be done as part of an assignment + * statement. + */ + default T metricsGauge(String name, T obj, ToDoubleFunction valueFunction) { + return globalRegistry().gauge(name, obj, valueFunction); + } + + /** + * Returns an empty histogram snapshot with the specified aggregate values. + * + * @param count count + * @param total total + * @param max max value + * @return histogram snapshot + */ + HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max); + +} diff --git a/metrics/micrometer/pom.xml b/metrics/micrometer/pom.xml new file mode 100644 index 00000000000..66102d9433d --- /dev/null +++ b/metrics/micrometer/pom.xml @@ -0,0 +1,58 @@ + + + + + 4.0.0 + + io.helidon.metrics + helidon-metrics-project + 4.0.0-SNAPSHOT + + helidon-metrics-micrometer + Helidon Metrics - Micrometer Adapter + Micrometer implementation of the Helidon metrics API + + + + io.helidon.metrics + helidon-metrics-api + + + io.micrometer + micrometer-core + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + io.helidon.nima.testing.junit5 + helidon-nima-testing-junit5-webserver + test + + + diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java new file mode 100644 index 00000000000..745d66cf203 --- /dev/null +++ b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +public class MicrometerMetricFactory { /* implements HelidonMetricFactory { + + @Override + public Tag tag_of(String key, String value) { + return MicrometerTag.of(key, value); + } + + @Override + public Tags tags_of(String key, String value) { + return MicrometerTags.of(key, value); + } + + @Override + public Tags tags_concat(Iterable tags, Iterable other) { + return MicrometerTags.concat(tags, other); + } + + @Override + public Tags tags_concat(Iterable tags, String... keyValues) { + return MicrometerTags.concat(tags, keyValues); + } + + @Override + public Tags tags_of(Iterable tags) { + return MicrometerTags.of(tags); + } + + @Override + public Tags tags_of(String... keyValues) { + return MicrometerTags.of(keyValues); + } + + @Override + public Tags tags_of(Tag... tags) { + return MicrometerTags.of(tags); + } + + @Override + public Tags tags_empty() { + return MicrometerTags.empty(); + } +*/ + +} diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java new file mode 100644 index 00000000000..1bac4a12f1e --- /dev/null +++ b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import io.helidon.metrics.api.Tag; + +class MicrometerTag implements Tag { + + static MicrometerTag of(String key, String value) { + return new MicrometerTag(io.micrometer.core.instrument.Tag.of(key, value)); + } + + static MicrometerTag of(io.micrometer.core.instrument.Tag mTag) { + return of(mTag.getKey(), mTag.getValue()); + } + + private final io.micrometer.core.instrument.Tag delegate; + + private MicrometerTag(io.micrometer.core.instrument.Tag delegate) { + this.delegate = delegate; + } + + @Override + public String key() { + return delegate.getKey(); + } + + @Override + public String value() { + return delegate.getValue(); + } +} diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java new file mode 100644 index 00000000000..67fb34ce0ed --- /dev/null +++ b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Iterator; +import java.util.stream.Stream; + +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Tags; + +class MicrometerTags implements Tags { + + private final io.micrometer.core.instrument.Tags delegate; + + static Tags of(String key, String value) { + return new MicrometerTags(io.micrometer.core.instrument.Tags.of(key, value)); + } + + static Tags concat(Iterable tags, Iterable other) { + return of(tags).and(other); + } + + static Tags concat(Iterable tags, String... keyValues) { + return of(tags).and(keyValues); + } + + static Tags of(Iterable tags) { + return new MicrometerTags(io.micrometer.core.instrument.Tags.of(toMTags(tags))); + } + + static Tags of(String... keyValues) { + return new MicrometerTags(io.micrometer.core.instrument.Tags.of(keyValues)); + } + + static Tags of(Tag... tags) { + return new MicrometerTags(io.micrometer.core.instrument.Tags.of(toMTags(tags))); + } + + static Tags empty() { + return new MicrometerTags(io.micrometer.core.instrument.Tags.empty()); + } + + private static Iterable toMTags(Iterable tags) { + return new Iterable<>() { + + private final Iterator tagIt = tags.iterator(); + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return tagIt.hasNext(); + } + + @Override + public io.micrometer.core.instrument.Tag next() { + Tag next = tagIt.next(); + return io.micrometer.core.instrument.Tag.of(next.key(), next.value()); + } + }; + } + }; + } + + private static io.micrometer.core.instrument.Tag[] toMTags(Tag[] tags) { + io.micrometer.core.instrument.Tag[] result = new io.micrometer.core.instrument.Tag[tags.length]; + for (int i = 0; i < tags.length; i++) { + result[i] = io.micrometer.core.instrument.Tag.of(tags[i].key(), tags[i].value()); + } + return result; + } + + private MicrometerTags(io.micrometer.core.instrument.Tags delegate) { + this.delegate = delegate; + } + + public Tags and(String key, String value) { + return new MicrometerTags(delegate.and(key, value)); + } + + public Tags and(String... keyValues) { + return new MicrometerTags(delegate.and(keyValues)); + } + + public Tags and(Tag... tags) { + return new MicrometerTags(delegate.and(toMTags(tags))); + } + + public Tags and(Iterable tags) { + return new MicrometerTags(delegate.and(toMTags(tags))); + } + + public Iterator iterator() { + return new Iterator<>() { + private final Iterator mTagIt = delegate.iterator(); + + @Override + public boolean hasNext() { + return mTagIt.hasNext(); + } + + @Override + public Tag next() { + var next = mTagIt.next(); + return MicrometerTag.of(next.getKey(), next.getValue()); + } + }; + } + + @Override + public Stream stream() { + return delegate.stream() + .map(MicrometerTag::of); + } +} diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java new file mode 100644 index 00000000000..b517b7a453f --- /dev/null +++ b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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. + */ + +/** + * Micrometer wrapper for Helidon metrics API. + */ +package io.helidon.metrics.micrometer; \ No newline at end of file diff --git a/metrics/micrometer/src/main/java/module-info.java b/metrics/micrometer/src/main/java/module-info.java new file mode 100644 index 00000000000..27f8f14f45c --- /dev/null +++ b/metrics/micrometer/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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. + */ + +import io.helidon.metrics.micrometer.MicrometerMetricFactory; + +/** + * Micrometer adapter for Helidon metrics API. + */ +module io.helidon.metrics.micrometer { + + requires io.helidon.metrics.api; + requires micrometer.core; + +// provides io.helidon.metrics.api.spi.HelidonMetricFactory with MicrometerMetricFactory; +} \ No newline at end of file From 4bb5e95ebbf2512a621c9652e82dda75bef8eb5c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 31 Jul 2023 17:19:37 -0500 Subject: [PATCH 03/40] Incorporate some review comments: move micrometer impl under providers; revise some method signatures; clean up a few things. More to come --- metrics/api/pom.xml | 4 - .../api/DistributionStatisticsConfig.java | 2 +- .../metrics/api/HistogramSnapshot.java | 8 +- .../metrics/api/MetricFactoryManager.java | 10 +- ...ry.java => NoOpMetricFactoryProvider.java} | 8 +- .../java/io/helidon/metrics/api/Wrapped.java | 13 +- .../spi/HelidonMetricFactoryProvider.java | 21 +++ .../MetricFactoryProvider.java} | 4 +- .../io/helidon/metrics/spi/package-info.java | 20 +++ metrics/api/src/main/java/module-info.java | 7 +- .../metrics/micrometer/MicrometerTags.java | 129 ------------------ metrics/pom.xml | 1 + metrics/{ => providers}/micrometer/pom.xml | 4 +- .../micrometer/MicrometerMetricFactory.java | 4 +- .../metrics/micrometer/MicrometerTag.java | 0 .../metrics/micrometer/package-info.java | 0 .../micrometer/src/main/java/module-info.java | 2 +- metrics/providers/pom.xml | 35 +++++ 18 files changed, 112 insertions(+), 160 deletions(-) rename metrics/api/src/main/java/io/helidon/metrics/api/{HelidonNoOpMetricFactory.java => NoOpMetricFactoryProvider.java} (92%) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java rename metrics/api/src/main/java/io/helidon/metrics/{api/spi/HelidonMetricFactory.java => spi/MetricFactoryProvider.java} (99%) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java delete mode 100644 metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java rename metrics/{ => providers}/micrometer/pom.xml (94%) rename metrics/{ => providers}/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java (92%) rename metrics/{ => providers}/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java (100%) rename metrics/{ => providers}/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java (100%) rename metrics/{ => providers}/micrometer/src/main/java/module-info.java (90%) create mode 100644 metrics/providers/pom.xml diff --git a/metrics/api/pom.xml b/metrics/api/pom.xml index af23d2cc263..502d4e5a9e8 100644 --- a/metrics/api/pom.xml +++ b/metrics/api/pom.xml @@ -50,10 +50,6 @@ org.eclipse.microprofile.metrics microprofile-metrics-api - - io.micrometer - micrometer-core - io.helidon.common.testing helidon-common-testing-junit5 diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index 50921bcb432..82d32279b38 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -91,7 +91,7 @@ public interface DistributionStatisticsConfig extends Wrapped { /** * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. */ - interface Builder extends io.helidon.common.Builder { + interface Builder extends Wrapped, io.helidon.common.Builder { /** * Updates the builder with non-null settings from the specified existing config. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java index 8202c15d22f..f783adcccd8 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -84,16 +84,16 @@ static HistogramSnapshot empty(long count, double total, double max) { /** * Returns the values at the configured percentiles for the histogram. * - * @return array of pairs of percentile and the histogram value at that percentile + * @return pairs of percentile and the histogram value at that percentile */ - ValueAtPercentile[] percentileValues(); + Iterable percentileValues(); /** * Returns information about each of the configured buckets for the histogram. * - * @return array of pairs of bucket value and count of observations in that bucket + * @return pairs of bucket value and count of observations in that bucket */ - CountAtBucket[] histogramCounts(); + Iterable histogramCounts(); /** * Dumps a summary of the snapshot to the specified {@link java.io.PrintStream} using the indicated scaling factor for diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java index 7abab9d2fc3..0498a42b8b4 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java @@ -19,10 +19,10 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; -import io.helidon.metrics.api.spi.HelidonMetricFactory; +import io.helidon.metrics.spi.MetricFactoryProvider; /** - * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.api.spi.HelidonMetricFactory}, + * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.spi.MetricFactoryProvider}, * using a default no-op implementation if no other is available. */ class MetricFactoryManager { @@ -30,9 +30,9 @@ class MetricFactoryManager { /** * Instance of the highest-weight implementation of {@code MetricFactory}. */ - static final LazyValue INSTANCE = - LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(HelidonMetricFactory.class)) - .addService(HelidonNoOpMetricFactory.create(), Double.MIN_VALUE) + static final LazyValue INSTANCE = + LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricFactoryProvider.class)) + .addService(NoOpMetricFactoryProvider.create(), Double.MIN_VALUE) .build() .iterator() .next()); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java similarity index 92% rename from metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java rename to metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java index ec4bd18bdce..6a87cfdc4b9 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HelidonNoOpMetricFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java @@ -18,17 +18,17 @@ import java.util.function.Function; import java.util.function.ToDoubleFunction; -import io.helidon.metrics.api.spi.HelidonMetricFactory; +import io.helidon.metrics.spi.MetricFactoryProvider; /** * No-op implementation of the {@link io.helidon.metrics.api.spi.MetricFactory} interface. */ -class HelidonNoOpMetricFactory implements HelidonMetricFactory { +class NoOpMetricFactoryProvider implements MetricFactoryProvider { private final MeterRegistry meterRegistry = null; - static HelidonNoOpMetricFactory create() { - return new HelidonNoOpMetricFactory(); + static NoOpMetricFactoryProvider create() { + return new NoOpMetricFactoryProvider(); } @Override diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java index 4abcd478895..e1a7cac7651 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java @@ -21,13 +21,18 @@ public interface Wrapped { /** - * Unwraps the meter registry as the specified type. + * Unwraps the wrapped item as the specified type. * - * @param c {@link Class} to which to cast this meter registry - * @return the meter registry cast as the requested type + * @param c {@link Class} to which to cast this object + * @return this object cast as the requested type * @param type to cast to */ default R unwrap(Class c) { - return c.cast(this); + if (c.isInstance(this)) { + return c.cast(this); + } + throw new IllegalArgumentException(String.format("Cannot provide an object of %s from an object of type %s", + c.getName(), + getClass().getName())); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java new file mode 100644 index 00000000000..90bf4eb66ee --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.spi; + +public interface HelidonMetricFactoryProvider { + + +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java similarity index 99% rename from metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java rename to metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java index c50a902f7d0..d03aa004010 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/spi/HelidonMetricFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.metrics.api.spi; +package io.helidon.metrics.spi; import java.util.function.Function; import java.util.function.ToDoubleFunction; @@ -36,7 +36,7 @@ * {@link Timer#start(io.helidon.metrics.api.MeterRegistry)} method. *

*/ -public interface HelidonMetricFactory { +public interface MetricFactoryProvider { /** * Returns the global meter registry. diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java b/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java new file mode 100644 index 00000000000..73977e902dd --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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. + */ + +/** + * SPI for Helidon metrics. + */ +package io.helidon.metrics.spi; \ No newline at end of file diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index 0f1e122e81b..c7f80a34967 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -16,6 +16,7 @@ import io.helidon.metrics.api.spi.ExemplarService; import io.helidon.metrics.api.spi.RegistryFactoryProvider; +import io.helidon.metrics.spi.MetricFactoryProvider; /** * Helidon metrics API. @@ -25,14 +26,16 @@ requires io.helidon.http; requires transitive io.helidon.common.config; - requires transitive microprofile.metrics.api; requires static io.helidon.config.metadata; - requires micrometer.core; + requires microprofile.metrics.api; exports io.helidon.metrics.api; exports io.helidon.metrics.api.spi; + exports io.helidon.metrics.spi; uses RegistryFactoryProvider; uses ExemplarService; uses io.helidon.metrics.api.MetricsProgrammaticSettings; + uses io.helidon.metrics.api.spi.MetricFactory; + uses MetricFactoryProvider; } diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java b/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java deleted file mode 100644 index 67fb34ce0ed..00000000000 --- a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTags.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 - * - * http://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.helidon.metrics.micrometer; - -import java.util.Iterator; -import java.util.stream.Stream; - -import io.helidon.metrics.api.Tag; -import io.helidon.metrics.api.Tags; - -class MicrometerTags implements Tags { - - private final io.micrometer.core.instrument.Tags delegate; - - static Tags of(String key, String value) { - return new MicrometerTags(io.micrometer.core.instrument.Tags.of(key, value)); - } - - static Tags concat(Iterable tags, Iterable other) { - return of(tags).and(other); - } - - static Tags concat(Iterable tags, String... keyValues) { - return of(tags).and(keyValues); - } - - static Tags of(Iterable tags) { - return new MicrometerTags(io.micrometer.core.instrument.Tags.of(toMTags(tags))); - } - - static Tags of(String... keyValues) { - return new MicrometerTags(io.micrometer.core.instrument.Tags.of(keyValues)); - } - - static Tags of(Tag... tags) { - return new MicrometerTags(io.micrometer.core.instrument.Tags.of(toMTags(tags))); - } - - static Tags empty() { - return new MicrometerTags(io.micrometer.core.instrument.Tags.empty()); - } - - private static Iterable toMTags(Iterable tags) { - return new Iterable<>() { - - private final Iterator tagIt = tags.iterator(); - - @Override - public Iterator iterator() { - return new Iterator() { - @Override - public boolean hasNext() { - return tagIt.hasNext(); - } - - @Override - public io.micrometer.core.instrument.Tag next() { - Tag next = tagIt.next(); - return io.micrometer.core.instrument.Tag.of(next.key(), next.value()); - } - }; - } - }; - } - - private static io.micrometer.core.instrument.Tag[] toMTags(Tag[] tags) { - io.micrometer.core.instrument.Tag[] result = new io.micrometer.core.instrument.Tag[tags.length]; - for (int i = 0; i < tags.length; i++) { - result[i] = io.micrometer.core.instrument.Tag.of(tags[i].key(), tags[i].value()); - } - return result; - } - - private MicrometerTags(io.micrometer.core.instrument.Tags delegate) { - this.delegate = delegate; - } - - public Tags and(String key, String value) { - return new MicrometerTags(delegate.and(key, value)); - } - - public Tags and(String... keyValues) { - return new MicrometerTags(delegate.and(keyValues)); - } - - public Tags and(Tag... tags) { - return new MicrometerTags(delegate.and(toMTags(tags))); - } - - public Tags and(Iterable tags) { - return new MicrometerTags(delegate.and(toMTags(tags))); - } - - public Iterator iterator() { - return new Iterator<>() { - private final Iterator mTagIt = delegate.iterator(); - - @Override - public boolean hasNext() { - return mTagIt.hasNext(); - } - - @Override - public Tag next() { - var next = mTagIt.next(); - return MicrometerTag.of(next.getKey(), next.getValue()); - } - }; - } - - @Override - public Stream stream() { - return delegate.stream() - .map(MicrometerTag::of); - } -} diff --git a/metrics/pom.xml b/metrics/pom.xml index fe55a9edab9..f7475c08a2d 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -31,6 +31,7 @@ metrics + providers prometheus trace-exemplar api diff --git a/metrics/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml similarity index 94% rename from metrics/micrometer/pom.xml rename to metrics/providers/micrometer/pom.xml index 66102d9433d..38f2eb518ed 100644 --- a/metrics/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -23,11 +23,11 @@ 4.0.0 io.helidon.metrics - helidon-metrics-project + helidon-metrics-providers-project 4.0.0-SNAPSHOT helidon-metrics-micrometer - Helidon Metrics - Micrometer Adapter + Helidon Metrics Micrometer Adapter Micrometer implementation of the Helidon metrics API diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java similarity index 92% rename from metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java index 745d66cf203..9f9f1b04537 100644 --- a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java @@ -15,10 +15,10 @@ */ package io.helidon.metrics.micrometer; -public class MicrometerMetricFactory { /* implements HelidonMetricFactory { +public class MicrometerMetricFactory { /* implements MetricFactoryProvider { @Override - public Tag tag_of(String key, String value) { + public Tag tagOf(String key, String value) { return MicrometerTag.of(key, value); } diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java similarity index 100% rename from metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java diff --git a/metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java similarity index 100% rename from metrics/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java diff --git a/metrics/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java similarity index 90% rename from metrics/micrometer/src/main/java/module-info.java rename to metrics/providers/micrometer/src/main/java/module-info.java index 27f8f14f45c..603aacade6a 100644 --- a/metrics/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -24,5 +24,5 @@ requires io.helidon.metrics.api; requires micrometer.core; -// provides io.helidon.metrics.api.spi.HelidonMetricFactory with MicrometerMetricFactory; +// provides io.helidon.metrics.spi.MetricFactoryProvider with MicrometerMetricFactory; } \ No newline at end of file diff --git a/metrics/providers/pom.xml b/metrics/providers/pom.xml new file mode 100644 index 00000000000..f156b501b7e --- /dev/null +++ b/metrics/providers/pom.xml @@ -0,0 +1,35 @@ + + + + + 4.0.0 + + io.helidon.metrics + helidon-metrics-project + 4.0.0-SNAPSHOT + + pom + helidon-metrics-providers-project + Helidon Metrics Providers Project + Providers of the Helidon Metrics API + + + micrometer + + From b8048960d848df62abe109c09fb04af18b3400af Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 31 Jul 2023 17:23:24 -0500 Subject: [PATCH 04/40] Other review comment; change recordCallable to record --- metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java | 2 +- metrics/api/src/main/java/io/helidon/metrics/api/Timer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 0bacc75f7c5..b15541eb899 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -353,7 +353,7 @@ public T record(Supplier f) { } @Override - public T recordCallable(Callable f) throws Exception { + public T record(Callable f) throws Exception { return null; } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index 9b2c947a718..25f6c20f3d3 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -88,7 +88,7 @@ static Sample start(Clock clock) { * @return return value from invoking the callable {@code f} * @throws Exception exception escaping from the callable */ - T recordCallable(Callable f) throws Exception; + T record(Callable f) throws Exception; /** * Executes the {@link java.lang.Runnable} {@code f} and records the time it takes. From 8b240b1e076fb8e3451f43d4de661df0831d67ed Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 31 Jul 2023 23:10:54 -0500 Subject: [PATCH 05/40] More review comment changes; fix up temporary dep. in service-api on MP metrics so it will build while I work on the neutral metrics API --- .../metrics/api/HistogramSnapshot.java | 2 +- .../java/io/helidon/metrics/api/Metrics.java | 4 ++-- ...nager.java => MetricsProviderManager.java} | 14 ++++++------- ...Provider.java => NoOpMetricsProvider.java} | 8 +++---- .../main/java/io/helidon/metrics/api/Tag.java | 2 +- .../java/io/helidon/metrics/api/Timer.java | 6 +++--- .../spi/HelidonMetricFactoryProvider.java | 21 ------------------- ...toryProvider.java => MetricsProvider.java} | 4 ++-- .../io/helidon/metrics/spi/package-info.java | 2 +- metrics/api/src/main/java/module-info.java | 4 ++-- ...ry.java => MicrometerMetricsProvider.java} | 5 ++++- .../metrics/micrometer/package-info.java | 2 +- .../micrometer/src/main/java/module-info.java | 4 +--- metrics/service-api/pom.xml | 5 +++++ .../src/main/java/module-info.java | 4 +++- 15 files changed, 37 insertions(+), 50 deletions(-) rename metrics/api/src/main/java/io/helidon/metrics/api/{MetricFactoryManager.java => MetricsProviderManager.java} (75%) rename metrics/api/src/main/java/io/helidon/metrics/api/{NoOpMetricFactoryProvider.java => NoOpMetricsProvider.java} (92%) delete mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java rename metrics/api/src/main/java/io/helidon/metrics/spi/{MetricFactoryProvider.java => MetricsProvider.java} (98%) rename metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/{MicrometerMetricFactory.java => MicrometerMetricsProvider.java} (92%) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java index f783adcccd8..82e22bd1215 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -32,7 +32,7 @@ public interface HistogramSnapshot extends Wrapped { * @return empty snapshot reporting the values as specified */ static HistogramSnapshot empty(long count, double total, double max) { - return MetricFactoryManager.INSTANCE.get().histogramSnapshotEmpty(count, total, max); + return MetricsProviderManager.INSTANCE.get().histogramSnapshotEmpty(count, total, max); } /** diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 390fa2aa250..3df86dd6fbb 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -29,7 +29,7 @@ public interface Metrics { * @return the global meter registry */ static MeterRegistry globalRegistry() { - return MetricFactoryManager.INSTANCE.get().globalRegistry(); + return MetricsProviderManager.INSTANCE.get().globalRegistry(); } /** @@ -193,6 +193,6 @@ static T gauge(String name, * @return new tag */ static Tag tag(String key, String value) { - return MetricFactoryManager.INSTANCE.get().tagOf(key, value); + return MetricsProviderManager.INSTANCE.get().tagOf(key, value); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java similarity index 75% rename from metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java rename to metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java index 0498a42b8b4..fe5d198ba8a 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java @@ -19,24 +19,24 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; -import io.helidon.metrics.spi.MetricFactoryProvider; +import io.helidon.metrics.spi.MetricsProvider; /** - * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.spi.MetricFactoryProvider}, + * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.spi.MetricsProvider}, * using a default no-op implementation if no other is available. */ -class MetricFactoryManager { +class MetricsProviderManager { /** * Instance of the highest-weight implementation of {@code MetricFactory}. */ - static final LazyValue INSTANCE = - LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricFactoryProvider.class)) - .addService(NoOpMetricFactoryProvider.create(), Double.MIN_VALUE) + static final LazyValue INSTANCE = + LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricsProvider.class)) + .addService(NoOpMetricsProvider.create(), Double.MIN_VALUE) .build() .iterator() .next()); - private MetricFactoryManager() { + private MetricsProviderManager() { } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java similarity index 92% rename from metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java rename to metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java index 6a87cfdc4b9..297a204ddad 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricFactoryProvider.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java @@ -18,17 +18,17 @@ import java.util.function.Function; import java.util.function.ToDoubleFunction; -import io.helidon.metrics.spi.MetricFactoryProvider; +import io.helidon.metrics.spi.MetricsProvider; /** * No-op implementation of the {@link io.helidon.metrics.api.spi.MetricFactory} interface. */ -class NoOpMetricFactoryProvider implements MetricFactoryProvider { +class NoOpMetricsProvider implements MetricsProvider { private final MeterRegistry meterRegistry = null; - static NoOpMetricFactoryProvider create() { - return new NoOpMetricFactoryProvider(); + static NoOpMetricsProvider create() { + return new NoOpMetricsProvider(); } @Override diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java index 42519f34d9b..f83c71b7479 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java @@ -42,6 +42,6 @@ public interface Tag extends Wrapped { * @return new {@code Tag} representing the key and value */ static Tag of(String key, String value) { - return MetricFactoryManager.INSTANCE.get().tagOf(key, value); + return MetricsProviderManager.INSTANCE.get().tagOf(key, value); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index 25f6c20f3d3..a1537ac5af7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -31,7 +31,7 @@ public interface Timer extends Meter, HistogramSupport { * @return new sample */ static Sample start() { - return MetricFactoryManager.INSTANCE.get().timerStart(); + return MetricsProviderManager.INSTANCE.get().timerStart(); } /** @@ -41,7 +41,7 @@ static Sample start() { * @return new sample with start time recorded */ static Sample start(MeterRegistry registry) { - return MetricFactoryManager.INSTANCE.get().timerStart(registry); + return MetricsProviderManager.INSTANCE.get().timerStart(registry); } /** @@ -51,7 +51,7 @@ static Sample start(MeterRegistry registry) { * @return new sample with start time recorded */ static Sample start(Clock clock) { - return MetricFactoryManager.INSTANCE.get().timerStart(clock); + return MetricsProviderManager.INSTANCE.get().timerStart(clock); } /** diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java deleted file mode 100644 index 90bf4eb66ee..00000000000 --- a/metrics/api/src/main/java/io/helidon/metrics/spi/HelidonMetricFactoryProvider.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 - * - * http://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.helidon.metrics.spi; - -public interface HelidonMetricFactoryProvider { - - -} diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java similarity index 98% rename from metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java rename to metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java index d03aa004010..6be87c1c672 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricFactoryProvider.java +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java @@ -27,7 +27,7 @@ import io.helidon.metrics.api.Timer; /** - * Factory for creating implementation instances of the Helidon metrics API. + * Behavior of implementations of the Helidon metrics API. *

* An implementation of this interface provides instance methods for each * of the static methods on the Helidon metrics API interfaces. The prefix of each method @@ -36,7 +36,7 @@ * {@link Timer#start(io.helidon.metrics.api.MeterRegistry)} method. *

*/ -public interface MetricFactoryProvider { +public interface MetricsProvider { /** * Returns the global meter registry. diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java b/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java index 73977e902dd..0ebcc9f61ac 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/package-info.java @@ -17,4 +17,4 @@ /** * SPI for Helidon metrics. */ -package io.helidon.metrics.spi; \ No newline at end of file +package io.helidon.metrics.spi; diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index c7f80a34967..6537af969d0 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -16,7 +16,7 @@ import io.helidon.metrics.api.spi.ExemplarService; import io.helidon.metrics.api.spi.RegistryFactoryProvider; -import io.helidon.metrics.spi.MetricFactoryProvider; +import io.helidon.metrics.spi.MetricsProvider; /** * Helidon metrics API. @@ -37,5 +37,5 @@ uses ExemplarService; uses io.helidon.metrics.api.MetricsProgrammaticSettings; uses io.helidon.metrics.api.spi.MetricFactory; - uses MetricFactoryProvider; + uses MetricsProvider; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java similarity index 92% rename from metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java index 9f9f1b04537..5aa775f9aef 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java @@ -15,7 +15,10 @@ */ package io.helidon.metrics.micrometer; -public class MicrometerMetricFactory { /* implements MetricFactoryProvider { +/** + * Implementation of Helidon metrics based on Micrometer. + */ +public class MicrometerMetricsProvider { /* implements MetricsProvider { @Override public Tag tagOf(String key, String value) { diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java index b517b7a453f..09353081fc0 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/package-info.java @@ -17,4 +17,4 @@ /** * Micrometer wrapper for Helidon metrics API. */ -package io.helidon.metrics.micrometer; \ No newline at end of file +package io.helidon.metrics.micrometer; diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index 603aacade6a..a7b6b06ab03 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -14,8 +14,6 @@ * limitations under the License. */ -import io.helidon.metrics.micrometer.MicrometerMetricFactory; - /** * Micrometer adapter for Helidon metrics API. */ @@ -24,5 +22,5 @@ requires io.helidon.metrics.api; requires micrometer.core; -// provides io.helidon.metrics.spi.MetricFactoryProvider with MicrometerMetricFactory; +// provides io.helidon.metrics.spi.MetricsProvider with MicrometerMetricsProvider; } \ No newline at end of file diff --git a/metrics/service-api/pom.xml b/metrics/service-api/pom.xml index 2ede487bce6..ee5bbc059b1 100644 --- a/metrics/service-api/pom.xml +++ b/metrics/service-api/pom.xml @@ -46,6 +46,11 @@ io.helidon.metrics helidon-metrics-api + + + org.eclipse.microprofile.metrics + microprofile-metrics-api + org.eclipse.parsson parsson diff --git a/metrics/service-api/src/main/java/module-info.java b/metrics/service-api/src/main/java/module-info.java index 7f89a7a5996..cfcf8d01a5c 100644 --- a/metrics/service-api/src/main/java/module-info.java +++ b/metrics/service-api/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,5 +24,7 @@ requires io.helidon.metrics.api; requires jakarta.json; + requires microprofile.metrics.api; + exports io.helidon.metrics.serviceapi; } From 90d93ecb8bc6394c19ce69442d40b49b09a43891 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 1 Aug 2023 01:30:37 -0500 Subject: [PATCH 06/40] Temporary for successful build while adding the neutral metrics API --- metrics/api/src/main/java/module-info.java | 2 +- metrics/service-api/pom.xml | 5 ----- metrics/service-api/src/main/java/module-info.java | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index 6537af969d0..e128322af0e 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -27,7 +27,7 @@ requires transitive io.helidon.common.config; requires static io.helidon.config.metadata; - requires microprofile.metrics.api; + requires transitive microprofile.metrics.api; exports io.helidon.metrics.api; exports io.helidon.metrics.api.spi; diff --git a/metrics/service-api/pom.xml b/metrics/service-api/pom.xml index ee5bbc059b1..2ede487bce6 100644 --- a/metrics/service-api/pom.xml +++ b/metrics/service-api/pom.xml @@ -46,11 +46,6 @@ io.helidon.metrics helidon-metrics-api - - - org.eclipse.microprofile.metrics - microprofile-metrics-api - org.eclipse.parsson parsson diff --git a/metrics/service-api/src/main/java/module-info.java b/metrics/service-api/src/main/java/module-info.java index cfcf8d01a5c..abe1941e6bb 100644 --- a/metrics/service-api/src/main/java/module-info.java +++ b/metrics/service-api/src/main/java/module-info.java @@ -24,7 +24,5 @@ requires io.helidon.metrics.api; requires jakarta.json; - requires microprofile.metrics.api; - exports io.helidon.metrics.serviceapi; } From 03f9a41fe8403be7a38ee37666b6fb887356348c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 2 Aug 2023 19:42:44 -0500 Subject: [PATCH 07/40] Two intermixed changes: trimming the API exposed by Metrics and MeterRegistry, and reworking factory and provider for obtaining a MeterRegistry from the highest-weight implementation. More work needed in the latter at least --- metrics/api/pom.xml | 29 ++ .../java/io/helidon/metrics/api/Counter.java | 8 + .../api/DistributionStatisticsConfig.java | 4 + .../metrics/api/DistributionSummary.java | 10 + .../java/io/helidon/metrics/api/Gauge.java | 13 + .../metrics/api/HistogramSnapshot.java | 2 +- .../helidon/metrics/api/HistogramSupport.java | 8 + ...rmanceIndicatorMetricsConfigBlueprint.java | 74 ++++ .../java/io/helidon/metrics/api/Meter.java | 50 +++ .../io/helidon/metrics/api/MeterRegistry.java | 382 ++---------------- .../java/io/helidon/metrics/api/Metrics.java | 199 ++++----- .../metrics/api/MetricsConfigBlueprint.java | 90 +++++ .../helidon/metrics/api/MetricsFactory.java | 150 +++++++ ...anager.java => MetricsFactoryManager.java} | 25 +- .../io/helidon/metrics/api/NoOpMeter.java | 86 ++-- .../metrics/api/NoOpMeterRegistry.java | 277 +------------ ...sProvider.java => NoOpMetricsFactory.java} | 56 +-- .../api/NoOpMetricsFactoryProvider.java | 32 ++ .../main/java/io/helidon/metrics/api/Tag.java | 2 +- .../java/io/helidon/metrics/api/Timer.java | 16 +- .../metrics/spi/MetricsFactoryProvider.java | 24 ++ .../helidon/metrics/spi/MetricsProvider.java | 235 ----------- metrics/api/src/main/java/module-info.java | 8 +- .../micrometer/MicrometerMetricsProvider.java | 2 +- .../micrometer/src/main/java/module-info.java | 2 +- 25 files changed, 739 insertions(+), 1045 deletions(-) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java rename metrics/api/src/main/java/io/helidon/metrics/api/{MetricsProviderManager.java => MetricsFactoryManager.java} (55%) rename metrics/api/src/main/java/io/helidon/metrics/api/{NoOpMetricsProvider.java => NoOpMetricsFactory.java} (54%) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactoryProvider.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java delete mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java diff --git a/metrics/api/pom.xml b/metrics/api/pom.xml index 502d4e5a9e8..3f2ccafe923 100644 --- a/metrics/api/pom.xml +++ b/metrics/api/pom.xml @@ -41,6 +41,20 @@ io.helidon.common helidon-common-config + + io.helidon.builder + helidon-builder-api + + + io.helidon.inject.configdriven + helidon-inject-configdriven-api + true + + + io.helidon.inject.configdriven + helidon-inject-configdriven-runtime + true + io.helidon.config helidon-config-metadata @@ -84,11 +98,26 @@ true + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + io.helidon.config helidon-config-metadata-processor ${helidon.version} + + io.helidon.inject.configdriven + helidon-inject-configdriven-processor + ${helidon.version} + + + io.helidon.builder + helidon-builder-processor + ${helidon.version} + diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java index 04f728a117d..42bdab8a542 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java @@ -20,6 +20,11 @@ */ public interface Counter extends Meter { + static Builder builder(String name) { + return MetricsFactory.getInstance().counterBuilder(name); + } + + /** * Updates the counter by one. */ @@ -38,4 +43,7 @@ public interface Counter extends Meter { * @return cumulative count since this counter was registered */ double count(); + + interface Builder extends Meter.Builder { + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index 82d32279b38..d2601493a4d 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -24,6 +24,10 @@ */ public interface DistributionStatisticsConfig extends Wrapped { + static Builder builder() { + return MetricsFactory.getInstance().distributionStatisticsConfigBuilder(); + } + /** * Returns whether the configuration is set for percentile histograms which can be aggregated for percentile approximations. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java index db1bf6d251b..aef4eedcd1f 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -20,6 +20,10 @@ */ public interface DistributionSummary extends Meter { + static Builder builder(String name) { + return MetricsFactory.getInstance().distributionSummaryBuilder(name); + } + /** * Updates the statistics kept by the summary with the specified amount. * @@ -55,4 +59,10 @@ public interface DistributionSummary extends Meter { * @return maximum value of recorded events */ double max(); + + interface Builder extends Meter.Builder { + Builder scale(double scale); + + Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder); + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java index 07990d827b1..69f41cf86db 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java @@ -15,12 +15,18 @@ */ package io.helidon.metrics.api; +import java.util.function.ToDoubleFunction; + /** * Measures a value that can increase or decrease and is updated by external logic, not by explicit invocations * of methods on this type. */ public interface Gauge extends Meter { + static Builder builder(String name, T stateObject, ToDoubleFunction fn) { + return MetricsFactory.getInstance().gaugeBuilder(name, stateObject, fn); + } + /** * Returns the value of the gauge. *

@@ -31,4 +37,11 @@ public interface Gauge extends Meter { * @return current value of the gauge */ double value(); + + interface Builder extends Meter.Builder, Gauge> { + +// T stateObject(); +// +// ToDoubleFunction fn(); + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java index 82e22bd1215..c3dfa29f23b 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -32,7 +32,7 @@ public interface HistogramSnapshot extends Wrapped { * @return empty snapshot reporting the values as specified */ static HistogramSnapshot empty(long count, double total, double max) { - return MetricsProviderManager.INSTANCE.get().histogramSnapshotEmpty(count, total, max); + return MetricsFactory.getInstance().histogramSnapshotEmpty(count, total, max); } /** diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java index 0f9568db08e..0e357560122 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java @@ -20,10 +20,18 @@ */ public interface HistogramSupport extends Meter { + + static Builder builder() { + return MetricsFactory.getInstance().histogramSupportBuilder(); + } + /** * Returns a snapshot of the data in a histogram. * * @return snapshot of the histogram */ HistogramSnapshot takeSnapshot(); + + interface Builder extends Meter.Builder { + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java new file mode 100644 index 00000000000..f254f403068 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import io.helidon.builder.api.Prototype; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.inject.configdriven.api.ConfigBean; + +@ConfigBean() +@Configured(prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY + +"." + + KeyPerformanceIndicatorMetricsConfigBlueprint.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY) +@Prototype.Blueprint() +interface KeyPerformanceIndicatorMetricsConfigBlueprint { + + /** + * Config key for extended key performance indicator metrics settings. + */ + String KEY_PERFORMANCE_INDICATORS_CONFIG_KEY = "key-performance-indicators"; + + /** + * Config key for {@code enabled} setting of the extended KPI metrics. + */ + String KEY_PERFORMANCE_INDICATORS_EXTENDED_CONFIG_KEY = "extended"; + + /** + * Default enabled setting for extended KPI metrics. + */ + String KEY_PERFORMANCE_INDICATORS_EXTENDED_DEFAULT = "false"; + + /** + * Config key for long-running requests threshold setting (in milliseconds). + */ + String LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY = "threshold-ms"; + + /** + * Config key for long-running requests settings. + */ + String LONG_RUNNING_REQUESTS_CONFIG_KEY = "long-running-requests"; + + /** + * Default long-running requests threshold. + */ + String LONG_RUNNING_REQUESTS_THRESHOLD_MS_DEFAULT = "10000"; // 10 seconds + + /** + * Configuration key for long-running requests extended configuration. + */ + String QUALIFIED_LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY = + LONG_RUNNING_REQUESTS_CONFIG_KEY + "." +LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY; + + @ConfiguredOption(key = KEY_PERFORMANCE_INDICATORS_EXTENDED_CONFIG_KEY, + value = KEY_PERFORMANCE_INDICATORS_EXTENDED_DEFAULT) + boolean isExtended(); + + @ConfiguredOption(key = QUALIFIED_LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY, + value = LONG_RUNNING_REQUESTS_THRESHOLD_MS_DEFAULT) + long longRunningRequestThresholdMs(); + +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index 23ed1055419..6e1ff42b2fa 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -20,6 +20,56 @@ */ public interface Meter extends Wrapped { + /** + * Common behavior of specific meter builders. + * + * @param type of the builder + * @param type of the meter the builder creates + */ + interface Builder, M extends Meter> { + + /** + * Returns the type-correct "this". + * + * @return properly-typed builder itself + */ + default B identity() { + return (B) this; + } + + /** + * Sets the tags to use in identifying the build meter. + * + * @param tags {@link io.helidon.metrics.api.Tag} instances to identify the meter + * @return updated builder + */ + B tags(Iterable tags); + + /** + * Sets the description. + * @param description meter description + * @return updated builder + */ + B description(String description); + + /** + * Sets the units. + * + * @param baseUnit meter unit + * @return updated builder + */ + B baseUnit(String baseUnit); + + +// String name(); +// +// Iterable tags(); +// +// String description(); +// +// String baseUnit(); + } + /** * Unique idenfier for a meter. */ diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 859738726b8..61390ea5084 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -18,22 +18,9 @@ import java.util.List; import java.util.Optional; import java.util.function.Predicate; -import java.util.function.ToDoubleFunction; /** * Manages the look-up and registration of meters. - * - *

- * This interface supports two types of retrieval (using {@link io.helidon.metrics.api.Counter} as an example): - *

    - *
  • retrieve or create - {@link #counter(String, Iterable)} - returns the meter if it was previously - * registered, otherwise creates and registers the meter
  • - *
  • retrieve only - {@link #getCounter(String, Iterable)} - returns an {@link java.util.Optional} - * for the meter, empty if the meter has not been registered and non-empty if it has been registered.
  • - *
- *

- * The meter registry uniquely identifies each meter by its name and tags (if any). - *

*/ public interface MeterRegistry extends Wrapped { @@ -53,358 +40,77 @@ public interface MeterRegistry extends Wrapped { Iterable meters(Predicate filter); /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its - * {@link io.helidon.metrics.api.Meter.Id}. - * - * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter - * @return new or previously-registered counter - */ - Counter counter(Meter.Id id); - - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its - * {@link io.helidon.metrics.api.Meter.Id}. - * - * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter - * @param target object which maintains the counter value - * @param fn function which, when applied to the target, yields the counter value - * @return new or previously-registered counter - * @param type of the target object - */ - Counter counter(Meter.Id id, - T target, - ToDoubleFunction fn); - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new + * one using the provided builder. * - * @param name counter name - * @param tags tags which further identify the counter - * @return new or previously-registered counter + * @param builder builder to use in finding or creating a meter + * @return the previously-registered meter with the same name and tags or, if none, the newly-registered one + * @param type of the meter + * @param builder for the meter */ - Counter counter(String name, - Iterable tags); + > M getOrCreate(B builder); /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * Locates a previously-registered counter. * - * @param name counter name - * @param tags key/value pairs for further identifying the counter; MUST be an even number of arguments - * @return new or previously-registered counter + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered counter; empty if not found */ - Counter counter(String name, - String... tags); + default Optional getCounter(String name, Iterable tags) { + return get(Counter.class, name, tags); + } /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. + * Locates a previously-registered distribution summary. * - * @param name counter name - * @param tags tags which further identify the counter - * @param baseUnit unit for the counter - * @param description counter description - * @return new or previously-registered counter + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered distribution summary; empty if not found */ - Counter counter(String name, - Iterable tags, - String baseUnit, - String description); + default Optional getSummary(String name, Iterable tags) { + return get(DistributionSummary.class, name, tags); + } /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags or registers a new one - * which wraps an external target object which provides the counter value. - * - *

- * The counter returned rejects attempts to increment its value because the external object, not the counter itself, - * maintains the value. - *

+ * Locates a previously-registered gauge. * - * @param id {@link io.helidon.metrics.api.Meter.Id} for the counter - * @param baseUnit unit for the counter - * @param description counter description - * @param target object which provides the counter value - * @param fn function which, when applied to the target, returns the counter value - * @return the target object - * @param type of the target object + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered gauge; empty if not found */ - Counter counter(Meter.Id id, - String baseUnit, - String description, - T target, - ToDoubleFunction fn); + default Optional getGauge(String name, Iterable tags) { + return get(Gauge.class, name, tags); + } /** - * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically - * increasing value that is maintained by an external object, not a counter furnished by the meter registry itself. - * - *

- * The counter returned rejects attempts to increment its value because the external object, not the counter itself, - * maintains the value. - *

+ * Locates a previously-registered timer. * - * @param name counter name - * @param tags further identification of the counter - * @param target object which, when the function is applied, yields the counter value - * @param fn function which produces the counter value - * @return new or existing counter - * @param type of the object which furnishes the counter value + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered timer; empty if not found */ - Counter counter(String name, - Iterable tags, - T target, - ToDoubleFunction fn); + default Optional getTimer(String name, Iterable tags) { + return get(Timer.class, name, tags); + } /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags or registers a new one - * which wraps an external target object which provides the counter value. - * + * Locates a previously-registered meter of the specified type, matching the name and tags. *

- * The counter returned rejects attempts to increment its value because the external object, not the counter itself, - * maintains the value. + * The method throws an {@link java.lang.ClassCastException} if a meter exists with + * the name and tags but is not type-compatible with the provided class. *

* - * @param name counter name - * @param tags tags which further identify the counter - * @param baseUnit unit for the counter - * @param description counter description - * @param target object which provides the counter value - * @param fn function which, when applied to the target, returns the counter value - * @return the target object - * @param type of the target object - */ - Counter counter(String name, - Iterable tags, - String baseUnit, - String description, - T target, - ToDoubleFunction fn); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. - * - * @param name counter name - * @param tags tags for further identifying the counter - * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise - */ - Optional getCounter(String name, - Iterable tags); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Counter} by its name and tags. - * - * @param name counter name - * @param tags tags for further identifying the counter - * @return {@link java.util.Optional} of {@code Counter} if found; {@code Optional.empty()} otherwise - */ - Optional getCounter(String name, - String... tags); - - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. - * - * @param id {@link io.helidon.metrics.api.Meter.Id} for the summary - * @param baseUnit unit for the counter - * @param description counter description - * @param distributionStatisticsConfig configuration governing distribution statistics calculations - * @param scale scaling factor to apply to every sample recorded by the summary - * @return new or existing summary - */ - DistributionSummary summary(Meter.Id id, - String baseUnit, - String description, - DistributionStatisticsConfig distributionStatisticsConfig, - double scale); - - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. - * - * @param name summary name - * @param tags tags for further identifying the summary - * @return new or existing distribution summary - */ - DistributionSummary summary(String name, - Iterable tags); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. - * - * @param name summary name to locate - * @param tags tags for further identifying the summary - * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise + * @param mClass type of the meter to find + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-regsitered meter; empty if not found + * @param type of the meter to find */ - Optional getSummary(String name, - Iterable tags); + Optional get(Class mClass, String name, Iterable tags); - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary}. - * - * @param name summary name - * @param tags key/value pairs for further identifying the summary; MUST be an even number of arguments - * @return new or existing distribution summary - */ - DistributionSummary summary(String name, - String... tags); - /** - * Locates a previously-registered {@link io.helidon.metrics.api.DistributionSummary} by its name and tags. - * - * @param name summary name to locate - * @param tags tags for further identifying the summary - * @return {@link java.util.Optional} of {@code DistributionSummary} if found; {@code Optional.empty()} otherwise - */ - Optional getSummary(String name, - Tag... tags); - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. - * - * @param name timer name - * @param tags tags for further identifying the timer - * @param baseUnit unit for the timer - * @param description timer description - * @param distributionStatisticsConfig configuration governing distribution statistics calculations - * @return new or existing timer - */ - Timer timer(String name, - Iterable tags, - String baseUnit, - String description, - DistributionStatisticsConfig distributionStatisticsConfig); - - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. - * - * @param name timer name - * @param tags tags for further identifying the timer - * @return new or existing timer - */ - Timer timer(String name, - Iterable tags); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. - * - * @param name timer name - * @param tags tags for further identifying the timer - * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise - */ - Optional getTimer(String name, - Iterable tags); - - /** - * Creates a new or locates a previously-registered {@link io.helidon.metrics.api.Timer}. - * - * @param name timer name - * @param tags tag key/value pairs for further identifying the timer; MUST be an even number of arguments - * @return new or existing timer - */ - Timer timer(String name, - String... tags); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Timer} by its name and tags. - * - * @param name timer name - * @param tags tags for further identifying the timer - * @return {@link java.util.Optional} of {@code Timer} if found; {@code Optional.empty()} otherwise - */ - Optional getTimer(String name, - Tag... tags); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object returned by applying - * the specified {@code valueFunction}. - * - * @param name name of the gauge - * @param tags tags for further identifying the gauge - * @param baseUnit base unit for the gauge - * @param description gauge description - * @param stateObject object to which the {@code valueFunction} is applied to obtain the gauge's value - * @param valueFunction function which, when applied to the {@code stateObject}, produces an instantaneous gauge value - * @param type of the state object which yields the gauge's value - * @return state object - */ - T gauge(String name, - Iterable tags, - String baseUnit, - String description, - T stateObject, - ToDoubleFunction valueFunction); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. - * - * @param name name of the gauge - * @param tags tags for further identifying the gauge - * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise - */ - Optional getGauge(String name, - Iterable tags); - - /** - * Locates a previously-registered {@link io.helidon.metrics.api.Gauge} by its name and tags. - * - * @param name name of the gauge - * @param tags tags for further identifying the gauge - * @return {@link java.util.Optional} of {@code Gauge} if found; {@code Optional.empty()} otherwise - */ - Optional getGauge(String name, - Tag... tags); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the specified {@link Number} - * instance. - * - * @param name name of the gauge - * @param tags tags for further identifying the gauge - * @param number thread-safe implementation of {@link Number} used to access the value - * @param type of the number from which the gauge value is extracted - * @return number argument passed (so the registration can be done as part of an assignment statement) - */ - T gauge(String name, - Iterable tags, - T number); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the {@link Number}. - * - * @param name name of the gauge - * @param number thread-safe implementation of {@link Number} used to access the gauge's value - * @param type of the state object from which the gauge value is extracted - * @return number argument passed (so the registration can be done as part of an assignment statement) - */ - T gauge(String name, - T number); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object by applying the specified - * function. - * - * @param name name of the gauge - * @param tags tags for further identifying the gauge - * @param stateObject state object used to compute a value - * @param valueFunction function which, when applied to the {@code stateObject}, yields an instantaneous gauge value - * @param type of the state object from which the gauge value is extracted - * @return state object argument passed (so the registration can be done as part - * of an assignment statement) - */ - T gauge(String name, - Iterable tags, - T stateObject, - ToDoubleFunction valueFunction); - - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge} that reports the value of the object by applying the specified - * function. - * - * @param name name of the gauge - * @param stateObject state object used to compute a value - * @param valueFunction function which, when applied to the {@code stateObject}, yields an instantaneous gauge value - * @param type of the state object from which the gauge value is extracted - * @return state object argument passed (so the registration can be done as part - * of an assignment statement) - */ - T gauge(String name, - T stateObject, - ToDoubleFunction valueFunction); /** * Removes a previously-registered meter. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 3df86dd6fbb..253333ff27e 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -15,7 +15,9 @@ */ package io.helidon.metrics.api; -import java.util.function.ToDoubleFunction; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Optional; /** * A main entry point to the Helidon metrics implementation, allowing access to the global meter registry and providing shortcut @@ -29,170 +31,121 @@ public interface Metrics { * @return the global meter registry */ static MeterRegistry globalRegistry() { - return MetricsProviderManager.INSTANCE.get().globalRegistry(); + return MetricsFactory.getInstance().globalRegistry(); } /** - * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically - * increasing value. + * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new + * one using the provided builder. * - * @param name counter name - * @param tags further identification of the counter; MUST be an even number of arguments representing key/value pairs - * of tags - * @return new or previously-registered counter + * @param builder builder to use in finding or creating a meter + * @return the previously-registered meter with the same name and tags or, if none, the newly-registered one + * @param type of the meter + * @param builder for the meter */ - static Counter counter(String name, String... tags) { - return globalRegistry().counter(name, tags); + static > M getOrCreate(B builder) { + return globalRegistry().getOrCreate(builder); } /** - * Registers a new or locates a previously-registered counter, using the global registry, which tracks a monotonically - * increasing value that is maintained by an external object, not a counter furnished by the meter registry itself. + * Locates a previously-registered counter. * - *

- * The counter returned rejects attempts to increment its value because the external object, not the counter itself, - * maintains the value. - *

- * - * @param name counter name - * @param tags further identification of the counter - * @param target object which, when the function is applied, yields the counter value - * @param fn function which produces the counter value - * @return new or existing counter - * @param type of the object which furnishes the counter value + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered counter; empty if not found */ - static Counter counter(String name, - Iterable tags, - T target, - ToDoubleFunction fn) { - return globalRegistry().counter(name, tags, target, fn); + static Optional getCounter(String name, Iterable tags) { + return globalRegistry().get(Counter.class, name, tags); } /** - * Registers a new or locates a previously-registered distribution summary, using the global registry, which measures the - * distribution of samples. + * Locates a previously-registered distribution summary. * - * @param name summary name - * @param tags further identification of the summary - * @return new or previously-registered distribution summary + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered distribution summary; empty if not found */ - static DistributionSummary summary(String name, - Iterable tags) { - return globalRegistry().summary(name, tags); + static Optional getSummary(String name, Iterable tags) { + return globalRegistry().get(DistributionSummary.class, name, tags); } /** - * Registers a new or locates a previously-registered distribution summary, using the global registry, which measures the - * distribution of samples. + * Locates a previously-registered gauge. * - * @param name summary name - * @param tags further identification of the summary; MUST be an even number of arguments representing key/value pairs - * of tags - * @return new or previously-registered distribution summary + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered gauge; empty if not found */ - static DistributionSummary summary(String name, - String... tags) { - return globalRegistry().summary(name, tags); + static Optional getGauge(String name, Iterable tags) { + return globalRegistry().get(Gauge.class, name, tags); } /** - * Registers a new or locates a previously-registered timer, using the global registry, which measures the time taken for - * short tasks and the count of those tasks. + * Locates a previously-registered timer. * - * @param name timer name - * @param tags further identification of the timer - * @return new or previously-registered timer + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-registered timer; empty if not found */ - static Timer timer(String name, - Iterable tags) { - return globalRegistry().timer(name, tags); + static Optional getTimer(String name, Iterable tags) { + return globalRegistry().get(Timer.class, name, tags); } /** - * Registers a new or locates a previously-registered timer, using the global registry, which measures the time taken for - * short tasks and the count of those tasks. + * Locates a previously-registered meter of the specified type, matching the name and tags. + *

+ * The method throws an {@link java.lang.IllegalArgumentException} if a meter exists with + * the name and tags but is not type-compatible with the provided class. + *

* - * @param name timer name - * @param tags further identification of the timer; MUST be an even number of arguments representing key/value pairs of tags. - * @return new or previously-registered timer + * @param mClass type of the meter to find + * @param name name to match + * @param tags tags to match + * @return {@link java.util.Optional} of the previously-regsitered meter; empty if not found + * @param type of the meter to find */ - static Timer timer(String name, - String... tags) { - return globalRegistry().timer(name, tags); + static Optional get(Class mClass, String name, Iterable tags) { + return globalRegistry().get(mClass, name, tags); } /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, that reports the double value - * maintained by the specified object and exposed by the object by applying the specified function. + * Creates a {@link Tag} for the specified key and value. * - * @param name name of the gauge - * @param tags further identification of the gauge - * @param obj object which exposes the gauge value - * @param valueFunction function which, when applied to the object, yields the gauge value - * @param type of the state object which maintains the gauge's value - * @return state object + * @param key tag key + * @param value tag value + * @return new tag */ - static T gauge(String name, - Iterable tags, - T obj, - ToDoubleFunction valueFunction) { - return globalRegistry().gauge(name, tags, obj, valueFunction); + static Tag tag(String key, String value) { + return MetricsFactory.getInstance().tagOf(key, value); } /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, which wraps a specific - * {@link java.lang.Number} instance. + * Provides an {@link java.lang.Iterable} of {@link io.helidon.metrics.api.Tag} over an array of tags. * - * @param name name of the gauge - * @param tags further identification of the gauge - * @param number thread-safe implementation of the specified subtype of {@link java.lang.Number} which is the gauge's value - * @param specific subtype of {@code Number} which the wrapped object exposes - * @return {@code number} wrapped by this gauge + * @param tags tags array to convert + * @return iterator over the tags */ - static N gauge(String name, - Iterable tags, - N number) { - return globalRegistry().gauge(name, tags, number); - } + static Iterable tags(Tag... tags) { + return () -> new Iterator<>() { - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, which wraps a specific - * {@link java.lang.Number} instance. - * - * @param name name of the gauge - * @param number thread-safe implementation of the specified subtype of {@link java.lang.Number} which is the gauge's value - * @param specific subtype of {@code Number} which the wrapped object exposes - * @return {@code number} wrapped by this gauge - */ - static N gauge(String name, - N number) { - return globalRegistry().gauge(name, number); - } + private int slot = 0; - /** - * Locates or registers a {@link io.helidon.metrics.api.Gauge}, using the global registry, that reports the double value - * maintained by the specified object and exposed by the object by applying the specified function. - * - * @param name name of the gauge - * @param obj object which exposes the gauge value - * @param valueFunction function which, when applied to the object, yields the gauge value - * @param type of the state object which maintains the gauge's value - * @return state object - */ - static T gauge(String name, - T obj, - ToDoubleFunction valueFunction) { - return globalRegistry().gauge(name, obj, valueFunction); - } + @Override + public boolean hasNext() { + return slot < tags.length; + } - /** - * Creates a {@link Tag} for the specified key and value. - * - * @param key tag key - * @param value tag value - * @return new tag - */ - static Tag tag(String key, String value) { - return MetricsProviderManager.INSTANCE.get().tagOf(key, value); + @Override + public Tag next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Tag result = MetricsFactoryManager.getInstance() + .tagOf(tags[slot].key(), + tags[slot].value()); + slot++; + return result; + } + }; } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java new file mode 100644 index 00000000000..a0fb43990de --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.helidon.builder.api.Prototype; +import io.helidon.common.config.GlobalConfig; +import io.helidon.config.metadata.Configured; +import io.helidon.config.metadata.ConfiguredOption; +import io.helidon.inject.configdriven.api.ConfigBean; + +/** + * Config bean for {@link io.helidon.metrics.api.MetricsConfig}. + */ +@ConfigBean() +@Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY) +@Prototype.Blueprint(builderInterceptor = MetricsConfigBlueprint.BuilderInterceptor.class) +interface MetricsConfigBlueprint { + + /** + * The config key containing settings for all of metrics. + */ + String METRICS_CONFIG_KEY = "metrics"; + + /** + * Config key for comma-separated, {@code tag=value} global tag settings. + */ + String GLOBAL_TAGS_CONFIG_KEY = "tags"; + + /** + * Config key for the app tag value to be applied to all metrics in this application. + */ + String APP_TAG_CONFIG_KEY = "appName"; + + + + /** + * Whether metrics functionality is enabled. + * + * @return if metrics are configured to be enabled + */ + @ConfiguredOption("true") + boolean enabled(); + + /** + * Key performance indicator metrics settings. + * + * @return key performance indicator metrics settings + */ + @ConfiguredOption(key = KeyPerformanceIndicatorMetricsConfigBlueprint.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY) + Optional keyPerformanceIndicatorMetricsConfig(); + +// /** +// * Global tags. +// * +// * @return name/value pairs for global tags +// */ +// @ConfiguredOption(key = GLOBAL_TAGS_CONFIG_KEY) +// Optional> globalTags(); + + @ConfiguredOption(key = APP_TAG_CONFIG_KEY) + Optional appTagValue(); + + class BuilderInterceptor implements Prototype.BuilderInterceptor> { + + @Override + public MetricsConfig.BuilderBase intercept(MetricsConfig.BuilderBase target) { + if (target.config().isEmpty()) { + target.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); + } + return target; + } + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java new file mode 100644 index 00000000000..6c08e4a77e4 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.function.ToDoubleFunction; + +/** + * Behavior of implementations of the Helidon metrics API. + *

+ * An implementation of this interface provides instance methods for each + * of the static methods on the Helidon metrics API interfaces. The prefix of each method + * here identifies the interface that bears the corresponding static method. For example, + * {@link #timerStart(io.helidon.metrics.api.MeterRegistry)} corresponds to the static + * {@link io.helidon.metrics.api.Timer#start(io.helidon.metrics.api.MeterRegistry)} method. + *

+ *

+ * ALso, various static methods create or return previously-created instances. + *

+ *

+ * Note that this is not intended to be the interface which developers use to work with Helidon metrics. + * Instead they should use the {@link io.helidon.metrics.api.Metrics} interface and its static convenience methods + * or use {@link io.helidon.metrics.api.Metrics#globalRegistry()} and use the returned + * {@link io.helidon.metrics.api.MeterRegistry} directly. + *

+ *

+ * Rather, implementations of Helidon metrics implement this interface and various internal parts of Helidon metrics, + * notably the static methods on {@link io.helidon.metrics.api.Metrics}, delegate to the highest-weight + * implementation of this interface. + *

+ */ +public interface MetricsFactory { + + static MetricsFactory getInstance() { + return MetricsFactoryManager.getInstance(); + } + + /** + * Returns the global meter registry. + * + * @return the global meter registry + */ + MeterRegistry globalRegistry(); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.Counter}. + * + * @param name name of the counter + * @return counter builder + */ + Counter.Builder counterBuilder(String name); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.DistributionStatisticsConfig}. + * + * @return statistics config builder + */ + DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder(); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param name name of the summary + * @return summary builder + */ + DistributionSummary.Builder distributionSummaryBuilder(String name); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.Gauge}. + * + * @param name name of the gauge + * @param stateObject object which maintains the value to be exposed via the gauge + * @param fn function which, when applied to the state object, returns the gauge value + * @return gauge builder + * @param type of the state object + */ + Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.HistogramSupport}. + * + * @return summary builder + */ + HistogramSupport.Builder histogramSupportBuilder(); + + /** + * Creates a builder for a {@link io.helidon.metrics.api.Timer}. + * + * @param name name of the timer + * @return timer builder + */ + Timer.Builder timerBuilder(String name); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using + * the system default {@link io.helidon.metrics.api.Clock}. + * + * @return new sample + */ + Timer.Sample timerStart(); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration, using the + * clock associated with the specified {@link io.helidon.metrics.api.MeterRegistry}. + * + * @param registry the meter registry whose {@link io.helidon.metrics.api.Clock} is to be used + * @return new sample with the start time recorded + */ + Timer.Sample timerStart(MeterRegistry registry); + + /** + * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using + * the specified {@link io.helidon.metrics.api.Clock}. + * + * @param clock the clock to use for measuring the duration + * @return new sample + */ + Timer.Sample timerStart(Clock clock); + + /** + * Creates a {@link io.helidon.metrics.api.Tag} from the specified key and value. + * + * @param key tag key + * @param value tag value + * @return new {@code Tag} instance + */ + Tag tagOf(String key, String value); + + /** + * Returns an empty histogram snapshot with the specified aggregate values. + * + * @param count count + * @param total total + * @param max max value + * @return histogram snapshot + */ + HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java similarity index 55% rename from metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java rename to metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java index fe5d198ba8a..f4eb48def79 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsProviderManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java @@ -19,24 +19,35 @@ import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; -import io.helidon.metrics.spi.MetricsProvider; +import io.helidon.common.config.GlobalConfig; +import io.helidon.metrics.spi.MetricsFactoryProvider; /** * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.spi.MetricsProvider}, * using a default no-op implementation if no other is available. */ -class MetricsProviderManager { +class MetricsFactoryManager { /** - * Instance of the highest-weight implementation of {@code MetricFactory}. + * Instance of the highest-weight implementation of {@link io.helidon.metrics.spi.MetricsFactoryProvider}. */ - static final LazyValue INSTANCE = - LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricsProvider.class)) - .addService(NoOpMetricsProvider.create(), Double.MIN_VALUE) + private static final LazyValue METRICS_FACTORY_PROVIDER = + LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricsFactoryProvider.class)) + .addService(NoOpMetricsFactoryProvider.create(), Double.MIN_VALUE) .build() .iterator() .next()); - private MetricsProviderManager() { + private static final LazyValue METRICS_FACTORY = + LazyValue.create(() -> METRICS_FACTORY_PROVIDER.get().create( + MetricsConfig.builder() + .config(GlobalConfig.config().get(MetricsConfig.METRICS_CONFIG_KEY)) + .build())); + + static MetricsFactory getInstance() { + return METRICS_FACTORY.get(); + } + + private MetricsFactoryManager() { } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index b15541eb899..d3b5a2d4029 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -27,6 +27,9 @@ import java.util.function.Supplier; import java.util.function.ToDoubleFunction; +/** + * No-op implementation of the Helidon {@link io.helidon.metrics.api.Meter} interface. + */ class NoOpMeter implements Meter { private final Id id; @@ -110,7 +113,7 @@ public Type type() { return type; } - abstract static class Builder, M extends Meter> implements io.helidon.common.Builder { + abstract static class Builder, M extends Meter> { private final String name; private final Map tags = new TreeMap<>(); // tree map for ordering by tag name @@ -123,39 +126,39 @@ private Builder(String name, Type type) { this.type = type; } - public abstract M build(); + abstract M build(); - B tags(String... tags) { - if (tags.length % 2 != 0) { - throw new IllegalArgumentException(""" - Tag list must contain an even number of items because they must \ - be key/value pairs")"""); - } - for (int slot = 0; slot < tags.length / 2; slot++) { - this.tags.put(tags[slot * 2], Tag.of(tags[slot * 2], tags[slot * 2 + 1])); - } - return identity(); - } - - B tags(Iterable tags) { + public B tags(Iterable tags) { tags.forEach(tag -> this.tags.put(tag.key(), tag)); return identity(); } - B tag(String key, String value) { + public B tag(String key, String value) { tags.put(key, Tag.of(key, value)); return identity(); } - B description(String description) { + public B description(String description) { this.description = description; return identity(); } - B baseUnit(String unit) { + public B baseUnit(String unit) { this.unit = unit; return identity(); } + + public B identity() { + return (B) this; + } + + String name() { + return name; + } + + Iterable tags() { + return tags.values(); + } } static class Counter extends NoOpMeter implements io.helidon.metrics.api.Counter { @@ -166,7 +169,7 @@ static Counter create(String name, Iterable tags) { .build(); } - static class Builder extends NoOpMeter.Builder { + static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.Counter.Builder { protected Builder(String name) { super(name, Type.COUNTER); @@ -208,7 +211,7 @@ public double count() { static class FunctionalCounter extends Counter { - static class Builder extends Counter.Builder { + static class Builder extends Counter.Builder implements io.helidon.metrics.api.Counter.Builder { private Builder(String name, T target, ToDoubleFunction fn) { super(name); @@ -237,7 +240,11 @@ public void increment(double amount) { static class DistributionSummary extends NoOpMeter implements io.helidon.metrics.api.DistributionSummary { - static class Builder extends NoOpMeter.Builder { + static class Builder extends NoOpMeter.Builder + implements io.helidon.metrics.api.DistributionSummary.Builder { + + private double scale; + private DistributionStatisticsConfig.Builder disitributionStatisticsConfigBuilder; private Builder(String name) { super(name, Type.DISTRIBUTION_SUMMARY); @@ -247,6 +254,18 @@ private Builder(String name) { public DistributionSummary build() { return new DistributionSummary(this); } + + @Override + public Builder scale(double scale) { + this.scale = scale; + return identity(); + } + + @Override + public Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + this.disitributionStatisticsConfigBuilder = distributionStatisticsConfigBuilder; + return identity(); + } } static DistributionSummary.Builder builder(String name) { @@ -284,14 +303,19 @@ public double max() { static class Gauge extends NoOpMeter implements io.helidon.metrics.api.Gauge { - static Builder builder(String name) { - return new Builder(name); + static Builder builder(String name, T stateObject, ToDoubleFunction fn) { + return new Builder<>(name, stateObject, fn); } - static class Builder extends NoOpMeter.Builder { + static class Builder extends NoOpMeter.Builder, Gauge> implements io.helidon.metrics.api.Gauge.Builder { - private Builder(String name) { + private final T stateObject; + private final ToDoubleFunction fn; + + private Builder(String name, T stateObject, ToDoubleFunction fn) { super(name, Type.GAUGE); + this.stateObject = stateObject; + this.fn = fn; } @Override @@ -300,7 +324,7 @@ public Gauge build() { } } - private Gauge(Builder builder) { + private Gauge(Builder builder) { super(builder); } @@ -312,7 +336,9 @@ public double value() { static class Timer extends NoOpMeter implements io.helidon.metrics.api.Timer { - static class Builder extends NoOpMeter.Builder { + static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.Timer.Builder { + + private DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder; private Builder(String name) { super(name, Type.TIMER); @@ -322,6 +348,12 @@ private Builder(String name) { public Timer build() { return new Timer(this); } + + @Override + public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + this.distributionStatisticsConfigBuilder = distributionStatisticsConfigBuilder; + return identity(); + } } static Builder builder(String name) { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 849b8efb88a..597ec0998ad 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -20,12 +20,10 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.function.ToDoubleFunction; /** * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}. @@ -91,278 +89,27 @@ public Meter remove(String name, Iterable tags) { } @Override - public Counter counter(Meter.Id id) { - return findOrRegister(id, - Counter.class, - () -> NoOpMeter.Counter.builder(id.name()) - .tags(id.tags()) - .build()); + public Optional get(Class mClass, String name, Iterable tags) { + return Optional.ofNullable(mClass.cast(meters.get(NoOpMeter.Id.create(name, tags)))); } @Override - public Counter counter(Meter.Id id, - T target, - ToDoubleFunction fn) { - return findOrRegister(id, - Counter.class, - () -> NoOpMeter.FunctionalCounter.builder(id.name(), target, fn) - .tags(id.tags()) - .build()); - } - - @Override - public Counter counter(String name, - Iterable tags, - String baseUnit, - String description, - T target, - ToDoubleFunction fn) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - Counter.class, - () -> NoOpMeter.FunctionalCounter.builder(name, target, fn) - .tags(tags) - .baseUnit(baseUnit) - .description(description) - .build()); - } - - @Override - public Counter counter(Meter.Id id, - String baseUnit, - String description, - T target, - ToDoubleFunction fn) { - return findOrRegister(id, - Counter.class, - () -> NoOpMeter.FunctionalCounter.builder(id.name(), target, fn) - .tags(id.tags()) - .baseUnit(baseUnit) - .description(description) - .build()); - } - - @Override - public Optional getCounter(String name, Iterable tags) { - return find(NoOpMeter.Id.create(name, tags), Counter.class); - } - - - @Override - public Counter counter(String name, Iterable tags) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - Counter.class, - () -> NoOpMeter.Counter.builder(name) - .tags(tags) - .build()); - } - - @Override - public Counter counter(String name, String... tags) { - Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); - return findOrRegister(id, - Counter.class, - () -> NoOpMeter.Counter.builder(name) - .tags(id.tags()) - .build()); - } - - @Override - public Counter counter(String name, - Iterable tags, - String baseUnit, - String description) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - Counter.class, - () -> NoOpMeter.Counter.builder(name) - .tags(tags) - .baseUnit(baseUnit) - .description(description) - .build()); - } - - @Override - public Counter counter(String name, - Iterable tags, - T target, - ToDoubleFunction fn) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - Counter.class, - () -> NoOpMeter.Counter.builder(name, target, fn) - .tags(tags) - .build()); - } - - @Override - public Optional getCounter(String name, String... tags) { - return find(NoOpMeter.Id.create(name, NoOpTag.tags(tags)), - Counter.class); - } - - - - @Override - public DistributionSummary summary(String name, Iterable tags) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - DistributionSummary.class, - () -> NoOpMeter.DistributionSummary.builder(name) - .tags(tags) - .build()); - } - - @Override - public DistributionSummary summary(Meter.Id id, - String baseUnit, - String description, - DistributionStatisticsConfig distributionStatisticsConfig, - double scale) { - return findOrRegister(id, - DistributionSummary.class, - () -> NoOpMeter.DistributionSummary.builder(id.name()) - .baseUnit(baseUnit) - .description(description) - .build()); - } - - @Override - public Optional getSummary(String name, Iterable tags) { - return find(NoOpMeter.Id.create(name, tags), - DistributionSummary.class); - } - - @Override - public Optional getSummary(String name, Tag... tags) { - return find(NoOpMeter.Id.create(name, tags), - DistributionSummary.class); - } - - - - @Override - public T gauge(String name, T stateObject, ToDoubleFunction valueFunction) { - // We don't need a variant of the builder to handle the state object and function because the no-op gauges - // don't need to operate anyway. - findOrRegister(NoOpMeter.Id.create(name, Set.of()), - Gauge.class, - () -> NoOpMeter.Gauge.builder(name) - .build()); - return stateObject; - } - - @Override - public T gauge(String name, Iterable tags, T stateObject, ToDoubleFunction valueFunction) { - findOrRegister(NoOpMeter.Id.create(name, tags), - Gauge.class, - () -> NoOpMeter.Gauge.builder(name) - .build()); - return stateObject; - } - - @Override - public T gauge(String name, Iterable tags, T number) { - findOrRegister(NoOpMeter.Id.create(name, tags), - Gauge.class, - () -> NoOpMeter.Gauge.builder(name) - .build()); - return number; - } - - @Override - public T gauge(String name, T number) { - findOrRegister(NoOpMeter.Id.create(name, Set.of()), - Gauge.class, - () -> NoOpMeter.Gauge.builder(name) - .build()); - return number; - } - - @Override - public T gauge(String name, - Iterable tags, - String baseUnit, - String description, - T stateObject, - ToDoubleFunction valueFunction) { - findOrRegister(NoOpMeter.Id.create(name, tags), - Gauge.class, - () -> NoOpMeter.Gauge.builder(name) - .baseUnit(baseUnit) - .description(description) - .build()); - return stateObject; - } - - @Override - public Optional getGauge(String name, Iterable tags) { - return find(NoOpMeter.Id.create(name, tags), - Gauge.class); - } - - @Override - public Optional getGauge(String name, Tag... tags) { - return find(NoOpMeter.Id.create(name, tags), - Gauge.class); - } - - @Override - public DistributionSummary summary(String name, String... tags) { - Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); - return findOrRegister(id, - DistributionSummary.class, - () -> NoOpMeter.DistributionSummary.builder(name) - .tags(id.tags()) - .build()); - } - - @Override - public Timer timer(String name, Iterable tags) { - return findOrRegister(NoOpMeter.Id.create(name, tags), - Timer.class, - () -> NoOpMeter.Timer.builder(name) - .tags(tags) - .build()); - } - - @Override - public Timer timer(String name, String... tags) { - Meter.Id id = NoOpMeter.Id.create(name, NoOpTag.tags(tags)); - return findOrRegister(id, - Timer.class, - () -> NoOpMeter.Timer.builder(name) - .tags(id.tags()) - .build()); - } - - @Override - public Timer timer(String name, - Iterable tags, - String baseUnit, - String description, - DistributionStatisticsConfig distributionStatisticsConfig) { - // The distribution is also a no-op, so we ignore the statistics config. - return findOrRegister(NoOpMeter.Id.create(name, tags), - Timer.class, - () -> NoOpMeter.Timer.builder(name) - .baseUnit(baseUnit) - .description(description) - .build()); - } - - @Override - public Optional getTimer(String name, Tag... tags) { - return find(NoOpMeter.Id.create(name, tags), - Timer.class); - } - - @Override - public Optional getTimer(String name, Iterable tags) { - return find(NoOpMeter.Id.create(name, tags), - Timer.class); + public > M getOrCreate(B builder) { + NoOpMeter.Builder b = (NoOpMeter.Builder) builder; + return findOrRegister(NoOpMeter.Id.create(b.name(), + b.tags()), + builder); } private Optional find(Meter.Id id, Class mClass) { return Optional.ofNullable(mClass.cast(meters.get(id))); } + private > M findOrRegister(Meter.Id id, B builder) { + NoOpMeter.Builder noOpBuilder = (NoOpMeter.Builder) builder; + return (M) meters.computeIfAbsent(id, + thidId -> noOpBuilder.build()); + } private M findOrRegister(Meter.Id id, Class mClass, Supplier meterSupplier) { // This next step is atomic because we are using a ConcurrentHashMap. Meter result = meters.computeIfAbsent(id, diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java similarity index 54% rename from metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java rename to metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 297a204ddad..61afa15d80a 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsProvider.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -15,20 +15,17 @@ */ package io.helidon.metrics.api; -import java.util.function.Function; import java.util.function.ToDoubleFunction; -import io.helidon.metrics.spi.MetricsProvider; - /** * No-op implementation of the {@link io.helidon.metrics.api.spi.MetricFactory} interface. */ -class NoOpMetricsProvider implements MetricsProvider { +class NoOpMetricsFactory implements MetricsFactory { private final MeterRegistry meterRegistry = null; - static NoOpMetricsProvider create() { - return new NoOpMetricsProvider(); + static NoOpMetricsFactory create() { + return new NoOpMetricsFactory(); } @Override @@ -42,60 +39,39 @@ public Tag tagOf(String key, String value) { } @Override - public Counter metricsCounter(String name, Iterable tags) { - return null; - } - - @Override - public Counter metricsCounter(String name, String... tags) { - return null; - } - - @Override - public Counter metricsCounter(String name, Iterable tags, T target, Function fn) { - return null; - } - - @Override - public DistributionSummary metricsSummary(String name, Iterable tags) { - return null; - } - - @Override - public DistributionSummary metricsSummary(String name, String... tags) { - return null; - } - - @Override - public Timer metricsTimer(String name, Iterable tags) { - return null; + public Counter.Builder counterBuilder(String name) { + return NoOpMeter.Counter.builder(name); } @Override - public Timer metricsTimer(String name, String... tags) { + public DistributionSummary.Builder distributionSummaryBuilder(String name) { + // TODO return null; } @Override - public T metricsGauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { - return null; + public Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn) { + return NoOpMeter.Gauge.builder(name, stateObject, fn); } @Override - public T metricsGauge(String name, Iterable tags, T number) { - return null; + public Timer.Builder timerBuilder(String name) { + return NoOpMeter.Timer.builder(name); } @Override - public T metricsGauge(String name, T number) { + public HistogramSupport.Builder histogramSupportBuilder() { + // TODO return null; } @Override - public T metricsGauge(String name, T obj, ToDoubleFunction valueFunction) { + public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() { + // TODO return null; } + // TODO fix remaining null returns @Override public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { return null; diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactoryProvider.java new file mode 100644 index 00000000000..85a8f7cbd9a --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactoryProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import io.helidon.metrics.spi.MetricsFactoryProvider; + +/** + * No-op implementation of {@link io.helidon.metrics.spi.MetricsFactoryProvider}. + */ +class NoOpMetricsFactoryProvider implements MetricsFactoryProvider { + static MetricsFactoryProvider create() { + return new NoOpMetricsFactoryProvider(); + } + + @Override + public MetricsFactory create(MetricsConfig metricsConfig) { + return NoOpMetricsFactory.create(); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java index f83c71b7479..a2b9b5857da 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java @@ -42,6 +42,6 @@ public interface Tag extends Wrapped { * @return new {@code Tag} representing the key and value */ static Tag of(String key, String value) { - return MetricsProviderManager.INSTANCE.get().tagOf(key, value); + return MetricsFactory.getInstance().tagOf(key, value); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index a1537ac5af7..f03763d59b7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -25,13 +25,17 @@ */ public interface Timer extends Meter, HistogramSupport { + static Builder builder(String name) { + return MetricsFactory.getInstance().timerBuilder(name); + } + /** * Starts a timing sample using the default system clock. * * @return new sample */ static Sample start() { - return MetricsProviderManager.INSTANCE.get().timerStart(); + return MetricsFactory.getInstance().timerStart(); } /** @@ -41,7 +45,7 @@ static Sample start() { * @return new sample with start time recorded */ static Sample start(MeterRegistry registry) { - return MetricsProviderManager.INSTANCE.get().timerStart(registry); + return MetricsFactory.getInstance().timerStart(registry); } /** @@ -51,7 +55,7 @@ static Sample start(MeterRegistry registry) { * @return new sample with start time recorded */ static Sample start(Clock clock) { - return MetricsProviderManager.INSTANCE.get().timerStart(clock); + return MetricsFactory.getInstance().timerStart(clock); } /** @@ -175,4 +179,10 @@ interface Sample { */ long stop(Timer timer); } + + interface Builder extends Meter.Builder { + + Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder); + + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java new file mode 100644 index 00000000000..7c3fef10512 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.spi; + +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; + +public interface MetricsFactoryProvider { + + MetricsFactory create(MetricsConfig metricsConfig); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java deleted file mode 100644 index 6be87c1c672..00000000000 --- a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsProvider.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 - * - * http://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.helidon.metrics.spi; - -import java.util.function.Function; -import java.util.function.ToDoubleFunction; - -import io.helidon.metrics.api.Clock; -import io.helidon.metrics.api.Counter; -import io.helidon.metrics.api.DistributionSummary; -import io.helidon.metrics.api.HistogramSnapshot; -import io.helidon.metrics.api.MeterRegistry; -import io.helidon.metrics.api.Tag; -import io.helidon.metrics.api.Timer; - -/** - * Behavior of implementations of the Helidon metrics API. - *

- * An implementation of this interface provides instance methods for each - * of the static methods on the Helidon metrics API interfaces. The prefix of each method - * here identifies the interface that bears the corresponding static method. For example, - * {@link #timerStart(io.helidon.metrics.api.MeterRegistry)} corresponds to the static - * {@link Timer#start(io.helidon.metrics.api.MeterRegistry)} method. - *

- */ -public interface MetricsProvider { - - /** - * Returns the global meter registry. - * - * @return the global meter registry - */ - MeterRegistry globalRegistry(); - - /** - * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using - * the system default {@link io.helidon.metrics.api.Clock}. - * - * @return new sample - */ - Timer.Sample timerStart(); - - /** - * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration, using the - * clock associated with the specified {@link io.helidon.metrics.api.MeterRegistry}. - * - * @param registry the meter registry whose {@link io.helidon.metrics.api.Clock} is to be used - * @return new sample with the start time recorded - */ - Timer.Sample timerStart(MeterRegistry registry); - - /** - * Returns a {@link io.helidon.metrics.api.Timer.Sample} for measuring a duration using - * the specified {@link io.helidon.metrics.api.Clock}. - * - * @param clock the clock to use for measuring the duration - * @return new sample - */ - Timer.Sample timerStart(Clock clock); - - /** - * Creates a {@link io.helidon.metrics.api.Tag} from the specified key and value. - * - * @param key tag key - * @param value tag value - * @return new {@code Tag} instance - */ - Tag tagOf(String key, String value); - - /** - * Tracks a monotonically increasing value. - * - * @param name The base metric name - * @param tags Sequence of dimensions for breaking down the name. - * @return A new or existing counter. - */ - default Counter metricsCounter(String name, Iterable tags) { - return globalRegistry().counter(name, tags); - } - - /** - * Tracks a monotonically increasing value. - * - * @param name The base metric name - * @param tags MUST be an even number of arguments representing key/value pairs of tags. - * @return A new or existing counter. - */ - default Counter metricsCounter(String name, String... tags) { - return globalRegistry().counter(name, tags); - } - - /** - * Tracks a monotonically increasing value as maintained by an external variable. - * - * @param name meter name - * @param tags tags for further identifying the meter - * @param target object which, when the function is applied, provides the counter value - * @param fn function which obtains the counter value from the target object - * @return counter - * @param type of the target object - */ - Counter metricsCounter(String name, Iterable tags, T target, Function fn); - - /** - * Measures the distribution of samples. - * - * @param name The base metric name - * @param tags Sequence of dimensions for breaking down the name. - * @return A new or existing distribution summary. - */ - default DistributionSummary metricsSummary(String name, Iterable tags) { - return globalRegistry().summary(name, tags); - } - - /** - * Creates a new {@link io.helidon.metrics.api.DistributionSummary}. - * - * @param name name of the new meter - * @param tags tags for identifying the new meter - * @return new {@code DistributionSummary} - */ - default DistributionSummary metricsSummary(String name, String... tags) { - return globalRegistry().summary(name, tags); - } - - /** - * Measures the time taken for short tasks and the count of these tasks. - * - * @param name The base metric name - * @param tags Sequence of dimensions for breaking down the name. - * @return A new or existing timer. - */ - default Timer metricsTimer(String name, Iterable tags) { - return globalRegistry().timer(name, tags); - } - - /** - * Measures the time taken for short tasks and the count of these tasks. - * - * @param name The base metric name - * @param tags MUST be an even number of arguments representing key/value pairs of tags. - * @return A new or existing timer. - */ - default Timer metricsTimer(String name, String... tags) { - return globalRegistry().timer(name, tags); - } - - /** - * Register a gauge that reports the value of the object after the function - * {@code valueFunction} is applied. The registration will keep a weak reference to the object so it will - * not prevent garbage collection. Applying {@code valueFunction} on the object should be thread safe. - *

- * If multiple gauges are registered with the same id, then the values will be aggregated and - * the sum will be reported. For example, registering multiple gauges for active threads in - * a thread pool with the same id would produce a value that is the overall number - * of active threads. For other behaviors, manage it on the user side and avoid multiple - * registrations. - * - * @param name Name of the gauge being registered. - * @param tags Sequence of dimensions for breaking down the name. - * @param obj Object used to compute a value. - * @param valueFunction Function that is applied on the value for the number. - * @param The type of the state object from which the gauge value is extracted. - * @return The number that was passed in so the registration can be done as part of an assignment - * statement. - */ - default T metricsGauge(String name, Iterable tags, T obj, ToDoubleFunction valueFunction) { - return globalRegistry().gauge(name, tags, obj, valueFunction); - } - - /** - * Register a gauge that reports the value of the {@link java.lang.Number}. - * - * @param name Name of the gauge being registered. - * @param tags Sequence of dimensions for breaking down the name. - * @param number Thread-safe implementation of {@link Number} used to access the value. - * @param The type of the state object from which the gauge value is extracted. - * @return The number that was passed in so the registration can be done as part of an assignment - * statement. - */ - default T metricsGauge(String name, Iterable tags, T number) { - return globalRegistry().gauge(name, tags, number); - } - - /** - * Register a gauge that reports the value of the {@link java.lang.Number}. - * - * @param name Name of the gauge being registered. - * @param number Thread-safe implementation of {@link Number} used to access the value. - * @param The type of the state object from which the gauge value is extracted. - * @return The number that was passed in so the registration can be done as part of an assignment - * statement. - */ - default T metricsGauge(String name, T number) { - return globalRegistry().gauge(name, number); - } - - /** - * Register a gauge that reports the value of the object. - * - * @param name Name of the gauge being registered. - * @param obj Object used to compute a value. - * @param valueFunction Function that is applied on the value for the number. - * @param The type of the state object from which the gauge value is extracted.F - * @return The number that was passed in so the registration can be done as part of an assignment - * statement. - */ - default T metricsGauge(String name, T obj, ToDoubleFunction valueFunction) { - return globalRegistry().gauge(name, obj, valueFunction); - } - - /** - * Returns an empty histogram snapshot with the specified aggregate values. - * - * @param count count - * @param total total - * @param max max value - * @return histogram snapshot - */ - HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max); - -} diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index e128322af0e..27b9fc4ebe3 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -16,7 +16,7 @@ import io.helidon.metrics.api.spi.ExemplarService; import io.helidon.metrics.api.spi.RegistryFactoryProvider; -import io.helidon.metrics.spi.MetricsProvider; +import io.helidon.metrics.api.MetricsFactory; /** * Helidon metrics API. @@ -26,8 +26,10 @@ requires io.helidon.http; requires transitive io.helidon.common.config; + requires io.helidon.builder.api; requires static io.helidon.config.metadata; requires transitive microprofile.metrics.api; + requires io.helidon.inject.configdriven.api; exports io.helidon.metrics.api; exports io.helidon.metrics.api.spi; @@ -36,6 +38,6 @@ uses RegistryFactoryProvider; uses ExemplarService; uses io.helidon.metrics.api.MetricsProgrammaticSettings; - uses io.helidon.metrics.api.spi.MetricFactory; - uses MetricsProvider; + uses io.helidon.metrics.spi.MetricsFactoryProvider; + uses MetricsFactory; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java index 5aa775f9aef..15653bbfde4 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java @@ -18,7 +18,7 @@ /** * Implementation of Helidon metrics based on Micrometer. */ -public class MicrometerMetricsProvider { /* implements MetricsProvider { +public class MicrometerMetricsProvider { /* implements MetricsFactory { @Override public Tag tagOf(String key, String value) { diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index a7b6b06ab03..d595ae1a793 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -22,5 +22,5 @@ requires io.helidon.metrics.api; requires micrometer.core; -// provides io.helidon.metrics.spi.MetricsProvider with MicrometerMetricsProvider; +// provides io.helidon.metrics.api.MetricsFactory with MicrometerMetricsProvider; } \ No newline at end of file From 6ccb69d34515521048f2b2d532c733023f21e51b Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 3 Aug 2023 08:50:24 -0500 Subject: [PATCH 08/40] Some clean-up of checkstyle, spotbugs. --- .../java/io/helidon/metrics/api/Counter.java | 9 +++++ .../api/DistributionStatisticsConfig.java | 5 +++ .../metrics/api/DistributionSummary.java | 22 +++++++++++ .../java/io/helidon/metrics/api/Gauge.java | 18 +++++++-- .../helidon/metrics/api/HistogramSupport.java | 9 ++++- ...rmanceIndicatorMetricsConfigBlueprint.java | 17 ++++++++- .../metrics/api/MetricsConfigBlueprint.java | 3 +- .../io/helidon/metrics/api/NoOpMeter.java | 14 ++----- .../metrics/api/NoOpMeterRegistry.java | 38 +++++++++---------- .../java/io/helidon/metrics/api/Timer.java | 15 ++++++++ .../metrics/spi/MetricsFactoryProvider.java | 9 +++++ 11 files changed, 121 insertions(+), 38 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java index 42bdab8a542..b5dd40b485b 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java @@ -20,6 +20,12 @@ */ public interface Counter extends Meter { + /** + * Creates a new builder for a counter. + * + * @param name counter name + * @return new builder + */ static Builder builder(String name) { return MetricsFactory.getInstance().counterBuilder(name); } @@ -44,6 +50,9 @@ static Builder builder(String name) { */ double count(); + /** + * Builder for a new counter. + */ interface Builder extends Meter.Builder { } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index d2601493a4d..d33c1ba232e 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -24,6 +24,11 @@ */ public interface DistributionStatisticsConfig extends Wrapped { + /** + * Creates a builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. + * + * @return new builder + */ static Builder builder() { return MetricsFactory.getInstance().distributionStatisticsConfigBuilder(); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java index aef4eedcd1f..a35c80323a3 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -20,6 +20,12 @@ */ public interface DistributionSummary extends Meter { + /** + * Creates a builder for a new {@link io.helidon.metrics.api.DistributionSummary}. + * + * @param name name for the summary + * @return new builder + */ static Builder builder(String name) { return MetricsFactory.getInstance().distributionSummaryBuilder(name); } @@ -60,9 +66,25 @@ static Builder builder(String name) { */ double max(); + /** + * Builder for a {@link io.helidon.metrics.api.DistributionSummary}. + */ interface Builder extends Meter.Builder { + + /** + * Sets the scale factor for observations recorded by the summary. + * + * @param scale scaling factor to apply to each observation + * @return updated builder + */ Builder scale(double scale); + /** + * Sets the config for distribution statistics for the distribution summary. + * + * @param distributionStatisticsConfigBuilder builder for the distribution statistics config + * @return updated builder + */ Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java index 69f41cf86db..6af8e144409 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java @@ -23,6 +23,15 @@ */ public interface Gauge extends Meter { + /** + * Creates a builder for creating a new gauge. + * + * @param name gauge name + * @param stateObject state object which maintains the gauge value + * @param fn function which, when applied to the state object, returns the gauge value + * @return new builder + * @param type of the state object + */ static Builder builder(String name, T stateObject, ToDoubleFunction fn) { return MetricsFactory.getInstance().gaugeBuilder(name, stateObject, fn); } @@ -38,10 +47,11 @@ static Builder builder(String name, T stateObject, ToDoubleFunction fn */ double value(); + /** + * Builder for a new gauge. + * + * @param type of the state object which exposes the gauge value. + */ interface Builder extends Meter.Builder, Gauge> { - -// T stateObject(); -// -// ToDoubleFunction fn(); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java index 0e357560122..3fae2a46a86 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java @@ -20,7 +20,11 @@ */ public interface HistogramSupport extends Meter { - + /** + * Creates a builder for a new {@link io.helidon.metrics.api.HistogramSupport} instance. + * + * @return new builder + */ static Builder builder() { return MetricsFactory.getInstance().histogramSupportBuilder(); } @@ -32,6 +36,9 @@ static Builder builder() { */ HistogramSnapshot takeSnapshot(); + /** + * Builder for a new {@link io.helidon.metrics.api.HistogramSupport}. + */ interface Builder extends Meter.Builder { } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java index f254f403068..effa46982ef 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java @@ -20,9 +20,12 @@ import io.helidon.config.metadata.ConfiguredOption; import io.helidon.inject.configdriven.api.ConfigBean; +/** + * Config bean for KPI metrics configuration. + */ @ConfigBean() @Configured(prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY - +"." + + "." + KeyPerformanceIndicatorMetricsConfigBlueprint.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY) @Prototype.Blueprint() interface KeyPerformanceIndicatorMetricsConfigBlueprint { @@ -61,12 +64,22 @@ interface KeyPerformanceIndicatorMetricsConfigBlueprint { * Configuration key for long-running requests extended configuration. */ String QUALIFIED_LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY = - LONG_RUNNING_REQUESTS_CONFIG_KEY + "." +LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY; + LONG_RUNNING_REQUESTS_CONFIG_KEY + "." + LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY; + /** + * Whether KPI extended metrics are enabled. + * + * @return true if KPI extended metrics are enabled; false otherwise + */ @ConfiguredOption(key = KEY_PERFORMANCE_INDICATORS_EXTENDED_CONFIG_KEY, value = KEY_PERFORMANCE_INDICATORS_EXTENDED_DEFAULT) boolean isExtended(); + /** + * Threshold in ms that characterizes whether a request is long running. + * + * @return threshold in ms indicating a long-running request + */ @ConfiguredOption(key = QUALIFIED_LONG_RUNNING_REQUESTS_THRESHOLD_CONFIG_KEY, value = LONG_RUNNING_REQUESTS_THRESHOLD_MS_DEFAULT) long longRunningRequestThresholdMs(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index a0fb43990de..4c8a4035817 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -15,8 +15,6 @@ */ package io.helidon.metrics.api; -import java.util.List; -import java.util.Map; import java.util.Optional; import io.helidon.builder.api.Prototype; @@ -66,6 +64,7 @@ interface MetricsConfigBlueprint { @ConfiguredOption(key = KeyPerformanceIndicatorMetricsConfigBlueprint.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY) Optional keyPerformanceIndicatorMetricsConfig(); +// TODO fix mapping // /** // * Global tags. // * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index d3b5a2d4029..c3cd0130b56 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -243,9 +243,6 @@ static class DistributionSummary extends NoOpMeter implements io.helidon.metrics static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.DistributionSummary.Builder { - private double scale; - private DistributionStatisticsConfig.Builder disitributionStatisticsConfigBuilder; - private Builder(String name) { super(name, Type.DISTRIBUTION_SUMMARY); } @@ -257,13 +254,12 @@ public DistributionSummary build() { @Override public Builder scale(double scale) { - this.scale = scale; return identity(); } @Override - public Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { - this.disitributionStatisticsConfigBuilder = distributionStatisticsConfigBuilder; + public Builder distributionStatisticsConfig( + DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { return identity(); } } @@ -338,8 +334,6 @@ static class Timer extends NoOpMeter implements io.helidon.metrics.api.Timer { static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.Timer.Builder { - private DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder; - private Builder(String name) { super(name, Type.TIMER); } @@ -350,8 +344,8 @@ public Timer build() { } @Override - public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { - this.distributionStatisticsConfigBuilder = distributionStatisticsConfigBuilder; + public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig( + DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { return identity(); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 597ec0998ad..641a0e8749d 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -23,7 +23,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; -import java.util.function.Supplier; /** * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}. @@ -110,22 +109,23 @@ private > M findOrRegister(Meter. return (M) meters.computeIfAbsent(id, thidId -> noOpBuilder.build()); } - private M findOrRegister(Meter.Id id, Class mClass, Supplier meterSupplier) { - // This next step is atomic because we are using a ConcurrentHashMap. - Meter result = meters.computeIfAbsent(id, - theId -> meterSupplier.get()); - - // Check the type in case we retrieved a previously-registered meter with the specified ID. The type will always - // be correct if we ran the supplier, in which this test is unneeded by mostly harmless. - // We could just attempt the cast and let Java throw a class cast exception itself if needed, but this is nicer. - if (!mClass.isInstance(result)) { - throw new IllegalArgumentException( - String.format("Found previously-registered meter with ID %s of type %s when expecting %s", - id, - result.getClass().getName(), - mClass.getName())); - } - - return mClass.cast(meters.put(id, meterSupplier.get())); - } +// TODO +// private M findOrRegister(Meter.Id id, Class mClass, Supplier meterSupplier) { +// // This next step is atomic because we are using a ConcurrentHashMap. +// Meter result = meters.computeIfAbsent(id, +// theId -> meterSupplier.get()); +// +// // Check the type in case we retrieved a previously-registered meter with the specified ID. The type will always +// // be correct if we ran the supplier, in which this test is unneeded by mostly harmless. +// // We could just attempt the cast and let Java throw a class cast exception itself if needed, but this is nicer. +// if (!mClass.isInstance(result)) { +// throw new IllegalArgumentException( +// String.format("Found previously-registered meter with ID %s of type %s when expecting %s", +// id, +// result.getClass().getName(), +// mClass.getName())); +// } +// +// return mClass.cast(meters.put(id, meterSupplier.get())); +// } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index f03763d59b7..14d1b4f9127 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -25,6 +25,12 @@ */ public interface Timer extends Meter, HistogramSupport { + /** + * Creates a builder for a new {@link io.helidon.metrics.api.Timer}. + * + * @param name timer name + * @return new builder + */ static Builder builder(String name) { return MetricsFactory.getInstance().timerBuilder(name); } @@ -180,8 +186,17 @@ interface Sample { long stop(Timer timer); } + /** + * Builder for a new {@link io.helidon.metrics.api.Timer}. + */ interface Builder extends Meter.Builder { + /** + * Configures the distribution statistics for the timer. + * + * @param distributionStatisticsConfigBuilder builder for the distribution statistics config + * @return updated builder + */ Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java index 7c3fef10512..5834f3f803b 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java @@ -18,7 +18,16 @@ import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; +/** + * Behavior of providers of {@link io.helidon.metrics.api.MetricsFactory} instances. + */ public interface MetricsFactoryProvider { + /** + * Creates a new {@link MetricsFactory} using the provided metrics config. + * + * @param metricsConfig metrics configuration settings + * @return new metrics factory + */ MetricsFactory create(MetricsConfig metricsConfig); } From a0398f6c9dc4deba440f0f0cfa8aca6b4ee08463 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 3 Aug 2023 09:46:17 -0500 Subject: [PATCH 09/40] Adapt to change in Prototype.Blueprint --- .../io/helidon/metrics/api/MetricsConfigBlueprint.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 4c8a4035817..0c5f1119d02 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -28,7 +28,7 @@ */ @ConfigBean() @Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY) -@Prototype.Blueprint(builderInterceptor = MetricsConfigBlueprint.BuilderInterceptor.class) +@Prototype.Blueprint(decorator = MetricsConfigBlueprint.BuilderDecorator.class) interface MetricsConfigBlueprint { /** @@ -76,14 +76,13 @@ interface MetricsConfigBlueprint { @ConfiguredOption(key = APP_TAG_CONFIG_KEY) Optional appTagValue(); - class BuilderInterceptor implements Prototype.BuilderInterceptor> { + class BuilderDecorator implements Prototype.BuilderDecorator> { @Override - public MetricsConfig.BuilderBase intercept(MetricsConfig.BuilderBase target) { + public void decorate(MetricsConfig.BuilderBase target) { if (target.config().isEmpty()) { target.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); } - return target; } } } From 3f10796f13bae8c4ecda112a9af5c190a73e9d28 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 3 Aug 2023 12:41:56 -0500 Subject: [PATCH 10/40] Add more tests of the neutral API; add some convenience methods --- metrics/api/pom.xml | 1 + .../java/io/helidon/metrics/api/Meter.java | 21 ++++++ .../io/helidon/metrics/api/MeterRegistry.java | 3 +- .../java/io/helidon/metrics/api/Metrics.java | 73 +++++++++++++++++++ .../helidon/metrics/api/MetricsFactory.java | 19 +++++ .../io/helidon/metrics/api/NoOpMeter.java | 18 +++++ .../metrics/api/NoOpMeterRegistry.java | 42 +++-------- .../metrics/api/NoOpMetricsFactory.java | 15 +++- 8 files changed, 157 insertions(+), 35 deletions(-) diff --git a/metrics/api/pom.xml b/metrics/api/pom.xml index 3f2ccafe923..7a70eac9be1 100644 --- a/metrics/api/pom.xml +++ b/metrics/api/pom.xml @@ -67,6 +67,7 @@ io.helidon.common.testing helidon-common-testing-junit5 + test org.junit.jupiter diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index 6e1ff42b2fa..64f1e377b7f 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -75,6 +75,27 @@ default B identity() { */ interface Id { + /** + * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name. + * + * @param name name for the ID + * @return new meter ID + */ + static Id of(String name) { + return MetricsFactoryManager.getInstance().idOf(name); + } + + /** + * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name and tags. + * + * @param name name for the ID + * @param tags tags for the ID + * @return new meter ID + */ + static Id of(String name, Iterable tags) { + return MetricsFactoryManager.getInstance().idOf(name, tags); + } + /** * Returns the meter name. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 61390ea5084..849f485fff7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.api; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.function.Predicate; @@ -37,7 +38,7 @@ public interface MeterRegistry extends Wrapped { * @param filter the predicate with which to evaluate each {@link io.helidon.metrics.api.Meter} * @return meters which match the predicate */ - Iterable meters(Predicate filter); + Collection meters(Predicate filter); /** * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 253333ff27e..efa7eccca21 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -18,6 +18,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.Set; /** * A main entry point to the Helidon metrics implementation, allowing access to the global meter registry and providing shortcut @@ -58,6 +59,16 @@ static Optional getCounter(String name, Iterable tags) { return globalRegistry().get(Counter.class, name, tags); } + /** + * Locates a previously-registerec counter. + * + * @param name name to match + * @return {@link java.util.Optional} of the previously-registered counter; empty if not found + */ + static Optional getCounter(String name) { + return getCounter(name, Set.of()); + } + /** * Locates a previously-registered distribution summary. * @@ -69,6 +80,16 @@ static Optional getSummary(String name, Iterable tags) return globalRegistry().get(DistributionSummary.class, name, tags); } + /** + * Locates a previously-registered distribution summary. + * + * @param name name to match + * @return {@link java.util.Optional} of the previously-registered distribution summary; empty if not found + */ + static Optional getSummary(String name) { + return getSummary(name, Set.of()); + } + /** * Locates a previously-registered gauge. * @@ -80,6 +101,16 @@ static Optional getGauge(String name, Iterable tags) { return globalRegistry().get(Gauge.class, name, tags); } + /** + * Locates a previously-registered gauge. + * + * @param name name to match + * @return {@link java.util.Optional} of the previously-registered gauge; empty if not found + */ + static Optional getGauge(String name) { + return getGauge(name, Set.of()); + } + /** * Locates a previously-registered timer. * @@ -91,6 +122,16 @@ static Optional getTimer(String name, Iterable tags) { return globalRegistry().get(Timer.class, name, tags); } + /** + * Locates a previously-registered timer. + * + * @param name name to match + * @return {@link java.util.Optional} of the previously-registered timer; empty if not found + */ + static Optional getTimer(String name) { + return getTimer(name, Set.of()); + } + /** * Locates a previously-registered meter of the specified type, matching the name and tags. *

@@ -148,4 +189,36 @@ public Tag next() { } }; } + + /** + * Returns an {@link java.lang.Iterable} of {@link io.helidon.metrics.api.Tag} by interpreting the provided strings as + * tag name/tag value pairs. + * + * @param keyValuePairs pairs of tag name/tag value pairs + * @return tags corresponding to the tag name/tag value pairs + */ + static Iterable tags(String... keyValuePairs) { + if (keyValuePairs.length % 2 != 0) { + throw new IllegalArgumentException("Must pass an even number of strings so keys and values are evenly matched"); + } + return () -> new Iterator<>() { + + private int slot; + + @Override + public boolean hasNext() { + return slot < keyValuePairs.length / 2; + } + + @Override + public Tag next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + Tag result = Tag.of(keyValuePairs[slot * 2], keyValuePairs[slot * 2 + 1]); + slot++; + return result; + } + }; + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index 6c08e4a77e4..9914d4d7ac4 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.api; +import java.util.Set; import java.util.function.ToDoubleFunction; /** @@ -129,6 +130,24 @@ static MetricsFactory getInstance() { */ Timer.Sample timerStart(Clock clock); + /** + * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name and tags. + * @param name name to use in the ID + * @param tags tags to use in the ID + * @return new meter ID + */ + Meter.Id idOf(String name, Iterable tags); + + /** + * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name. + * + * @param name name to use in the ID + * @return new meter ID + */ + default Meter.Id idOf(String name) { + return idOf(name, Set.of()); + } + /** * Creates a {@link io.helidon.metrics.api.Tag} from the specified key and value. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index c3cd0130b56..dd3005f5377 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -21,6 +21,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -77,6 +78,23 @@ String tag(String key) { .findFirst() .orElse(null); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Id id = (Id) o; + return name.equals(id.name) && tags.equals(id.tags); + } + + @Override + public int hashCode() { + return Objects.hash(name, tags); + } } private NoOpMeter(NoOpMeter.Builder builder) { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 641a0e8749d..397d1bff22d 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -15,10 +15,10 @@ */ package io.helidon.metrics.api; -import java.util.Iterator; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; @@ -39,37 +39,15 @@ public List meters() { } @Override - public Iterable meters(Predicate filter) { - return () -> new Iterator<>() { - - private final Iterator iter = meters.values().iterator(); - private Meter nextMatch = nextMatch(); - - private Meter nextMatch() { - while (iter.hasNext()) { - Meter candidate = iter.next(); - if (filter.test(candidate)) { - return candidate; + public Collection meters(Predicate filter) { + List result = new ArrayList<>(); + meters.values() + .forEach(m -> { + if (filter.test(m)) { + result.add(m); } - } - return null; - } - - @Override - public boolean hasNext() { - return nextMatch != null; - } - - @Override - public Meter next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - Meter result = nextMatch; - nextMatch = nextMatch(); - return result; - } - }; + }); + return result; } @Override diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 61afa15d80a..14a92b70730 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.api; +import java.util.Set; import java.util.function.ToDoubleFunction; /** @@ -22,7 +23,7 @@ */ class NoOpMetricsFactory implements MetricsFactory { - private final MeterRegistry meterRegistry = null; + private final MeterRegistry meterRegistry = new NoOpMeterRegistry(); static NoOpMetricsFactory create() { return new NoOpMetricsFactory(); @@ -30,7 +31,17 @@ static NoOpMetricsFactory create() { @Override public MeterRegistry globalRegistry() { - return null; + return meterRegistry; + } + + @Override + public Meter.Id idOf(String name) { + return idOf(name, Set.of()); + } + + @Override + public Meter.Id idOf(String name, Iterable tags) { + return NoOpMeter.Id.create(name, tags); } @Override From f43c1c82dda982c557cca82c207f37c2cb2e52ae Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 3 Aug 2023 13:02:10 -0500 Subject: [PATCH 11/40] Neutral API tests --- .../io/helidon/metrics/api/SimpleApiTest.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java diff --git a/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java new file mode 100644 index 00000000000..e4639415a96 --- /dev/null +++ b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.helidon.common.testing.junit5.OptionalMatcher; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; + +public class SimpleApiTest { + + private static final String COUNTER_1_DESC = "counter 1 description"; + + private static Counter counter1; + private static Counter counter2; + private static Timer timer1; + + @BeforeAll + static void prep() { + MeterRegistry registry = Metrics.globalRegistry(); + assertThat("Global registry", registry, notNullValue()); + counter1 = Metrics.getOrCreate(Counter.builder("counter1") + .description(COUNTER_1_DESC)); + counter2 = Metrics.getOrCreate(Counter.builder("counter2")); + + timer1 = Metrics.getOrCreate(Timer.builder("timer1") + .tags(Metrics.tags("t1", "v1", + "t2", "v2"))); + } + + @Test + void testNoOpRegistrations() { + + Optional fetchedCounter = Metrics.getCounter("counter1"); + assertThat("Fetched counter 1", + fetchedCounter.map(Counter::description), + OptionalMatcher.optionalValue(is(COUNTER_1_DESC))); + + fetchedCounter = Metrics.getCounter("counter2", Set.of()); + assertThat("Fetched counter 2", + fetchedCounter.map(Meter::description), + OptionalMatcher.optionalEmpty()); + + Optional fetchedTimer = Metrics.getTimer("timer1", Metrics.tags("t1", "v1", + "t2", "v2")); + assertThat("Fetched timer", + fetchedTimer.map(Meter::baseUnit), + OptionalMatcher.optionalEmpty()); + + } + + @Test + void testAllMetersFetch() { + Meter meter = Metrics.globalRegistry() + .meters() + .stream() + .filter(m -> m.id().name().equals("counter1")) + .findFirst() + .orElse(null); + + assertThat("Counter1 via meters()", meter, sameInstance(counter1)); + } + + @Test + void testFilteredMetersFetch() { + List candidateCounters = new ArrayList<>(Metrics.globalRegistry() + .meters(m -> m.id().name().equals("counter1"))); + + assertThat("Results", candidateCounters, hasSize(1)); + assertThat("Single result", candidateCounters.get(0), instanceOf(Counter.class)); + assertThat("Result name", candidateCounters.get(0).id().name(), is(equalTo("counter1"))); + assertThat("Result", candidateCounters.get(0), sameInstance(counter1)); + } + + @Test + void testFilteredMetersWithNoMatches() { + Collection candidateCounters = + Metrics.globalRegistry() + .meters(m -> m.id().name().equals("no such meter")); + + assertThat("Results", candidateCounters, hasSize(0)); + } + + @Test + void testRemoval() { + MeterRegistry reg = Metrics.globalRegistry(); + + assertThat("Precheck of test counter", + Metrics.getCounter("doomedCounter"), + OptionalMatcher.optionalEmpty()); + + reg.getOrCreate(Counter.builder("doomedCounter") + .description("doomed counter") + ); + + reg.remove("doomedCounter", Set.of()); + + assertThat("Post-check of doomed counter", + Metrics.getCounter("doomedCounter"), + OptionalMatcher.optionalEmpty()); + + } +} From 30762535c7730198eb7118fe633a7763a7330e3e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 4 Aug 2023 22:09:45 -0500 Subject: [PATCH 12/40] WIP adding Micrometer implementation; pushing for safekeeping even though these changes will not build --- .../api/DistributionStatisticsConfig.java | 58 +++-- .../metrics/api/DistributionSummary.java | 6 +- .../java/io/helidon/metrics/api/Meter.java | 48 +++- .../io/helidon/metrics/api/MeterRegistry.java | 12 +- .../helidon/metrics/api/MetricsFactory.java | 2 +- .../io/helidon/metrics/api/NoOpMeter.java | 12 +- .../metrics/api/NoOpMetricsFactory.java | 2 +- .../io/helidon/metrics/api/SimpleApiTest.java | 2 +- metrics/providers/micrometer/pom.xml | 4 + ...MicrometerTag.java => MCountAtBucket.java} | 29 ++- .../helidon/metrics/micrometer/MCounter.java | 65 +++++ .../MDistributionStatisticsConfig.java | 223 ++++++++++++++++++ .../micrometer/MDistributionSummary.java | 97 ++++++++ .../io/helidon/metrics/micrometer/MGauge.java | 56 +++++ .../micrometer/MHistogramSnapshot.java | 118 +++++++++ .../io/helidon/metrics/micrometer/MMeter.java | 190 +++++++++++++++ .../metrics/micrometer/MMeterRegistry.java | 109 +++++++++ .../io/helidon/metrics/micrometer/MTag.java | 90 +++++++ .../io/helidon/metrics/micrometer/MTimer.java | 137 +++++++++++ .../micrometer/MValueAtPercentile.java | 48 ++++ .../micrometer/MeterRegistryProvider.java | 51 ++++ .../micrometer/MicrometerMetricsProvider.java | 77 ++++-- .../io/helidon/metrics/micrometer/Util.java | 64 +++++ .../micrometer/src/main/java/module-info.java | 1 + 24 files changed, 1421 insertions(+), 80 deletions(-) rename metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/{MicrometerTag.java => MCountAtBucket.java} (56%) create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index d33c1ba232e..2d6ae410d22 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -16,6 +16,7 @@ package io.helidon.metrics.api; import java.time.Duration; +import java.util.Optional; /** * Configuration which controls the behavior of distribution statistics from meters that support them @@ -33,47 +34,63 @@ static Builder builder() { return MetricsFactory.getInstance().distributionStatisticsConfigBuilder(); } + /** + * Creates a new configuration by merging another one (called the "parent") with the current instance, + * using values from the current instance if they have been set and from the parent otherwise. + * + * @param parent the other configuration + * @return new config resulting from the merge + */ + DistributionStatisticsConfig merge(DistributionStatisticsConfig parent); + /** * Returns whether the configuration is set for percentile histograms which can be aggregated for percentile approximations. * * @return whether percentile histograms are configured */ - boolean isPercentileHistogram(); + Optional isPercentileHistogram(); /** * Returns whether the configuration is set to publish percentiles. * * @return true/false */ - boolean isPublishingPercentiles(); + Optional isPublishingPercentiles(); /** * Returns whether the configuration is set to publish a histogram. * * @return true/false */ - boolean isPublishingHistogram(); + Optional isPublishingHistogram(); /** * Returns the settings for non-aggregable percentiles. * * @return percentiles to compute and publish */ - Iterable percentiles(); + Optional> percentiles(); /** * Returns the configured number of digits of precision for percentiles. * * @return digits of precision to maintain for percentile approximations */ - int percentilePrecision(); + Optional percentilePrecision(); + + /** + * Returns the minimum expected value that the meter is expected to observe. + * + * @return minimum expected value + */ + Optional minimumExpectedValue(); /** * Returns the maximum value that the meter is expected to observe. * - * @return maximum value that the meter is expected to observe + * @return maximum value */ - double maximumExpectedValue(); + Optional maximumExpectedValue(); /** * Returns how long decaying past observations remain in the ring buffer. @@ -81,36 +98,27 @@ static Builder builder() { * @see #bufferLength() * @return time during which samples accumulate in a histogram */ - Duration expiry(); + Optional expiry(); /** * Returns the size of the ring buffer for holding decaying observations. * * @return number of observations to keep in the ring buffer */ - int bufferLength(); + Optional bufferLength(); /** * Returns the configured service level objective boundaries. * * @return the SLO boundaries */ - Iterable serviceLevelObjectiveBoundaries(); + Optional> serviceLevelObjectiveBoundaries(); /** * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. */ interface Builder extends Wrapped, io.helidon.common.Builder { - /** - * Updates the builder with non-null settings from the specified existing config. - * - * @param config the {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance to use to - * set values in this builder - * @return updated builder - */ - Builder merge(DistributionStatisticsConfig config); - /** * Sets how long to keep samples before they are assumed to have decayed to zero and are discareded. * @@ -125,7 +133,7 @@ interface Builder extends Wrapped, io.helidon.common.Builder tags(); +// +// /** +// * Returns the description assigned to the builder. +// * +// * @return description +// */ +// String description(); +// +// /** +// * Returns the unit assigned to the builder. +// * +// * @return unit +// */ +// String baseUnit(); +// +// } + /** * Common behavior of specific meter builders. * * @param type of the builder * @param type of the meter the builder creates */ - interface Builder, M extends Meter> { + interface Builder, M extends Meter> /* extends BuilderAdapter */ { /** * Returns the type-correct "this". @@ -59,15 +94,6 @@ default B identity() { * @return updated builder */ B baseUnit(String baseUnit); - - -// String name(); -// -// Iterable tags(); -// -// String description(); -// -// String baseUnit(); } /** @@ -108,7 +134,7 @@ static Id of(String name, Iterable tags) { * * @return meter tags */ - Iterable tags(); + Iterable tags(); /** * Unwraps the ID as the specified type. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 849f485fff7..d9e898b22f4 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -30,7 +30,7 @@ public interface MeterRegistry extends Wrapped { * * @return registered meters */ - List meters(); + List meters(); /** * Returns previously-registered meters which match the specified {@link java.util.function.Predicate}. @@ -38,7 +38,7 @@ public interface MeterRegistry extends Wrapped { * @param filter the predicate with which to evaluate each {@link io.helidon.metrics.api.Meter} * @return meters which match the predicate */ - Collection meters(Predicate filter); + Collection meters(Predicate filter); /** * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new @@ -58,7 +58,7 @@ public interface MeterRegistry extends Wrapped { * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered counter; empty if not found */ - default Optional getCounter(String name, Iterable tags) { + default Optional getCounter(String name, Iterable tags) { return get(Counter.class, name, tags); } @@ -69,7 +69,7 @@ default Optional getCounter(String name, Iterable tags) { * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered distribution summary; empty if not found */ - default Optional getSummary(String name, Iterable tags) { + default Optional getSummary(String name, Iterable tags) { return get(DistributionSummary.class, name, tags); } @@ -80,7 +80,7 @@ default Optional getSummary(String name, Iterable tags * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered gauge; empty if not found */ - default Optional getGauge(String name, Iterable tags) { + default Optional getGauge(String name, Iterable tags) { return get(Gauge.class, name, tags); } @@ -91,7 +91,7 @@ default Optional getGauge(String name, Iterable tags) { * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered timer; empty if not found */ - default Optional getTimer(String name, Iterable tags) { + default Optional getTimer(String name, Iterable tags) { return get(Timer.class, name, tags); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index 9914d4d7ac4..c0d3cef46ca 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -76,7 +76,7 @@ static MetricsFactory getInstance() { * @param name name of the summary * @return summary builder */ - DistributionSummary.Builder distributionSummaryBuilder(String name); + DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder); /** * Creates a builder for a {@link io.helidon.metrics.api.Gauge}. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index dd3005f5377..21e0fc8a283 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -170,13 +170,21 @@ public B identity() { return (B) this; } - String name() { + public String name() { return name; } - Iterable tags() { + public Iterable tags() { return tags.values(); } + + public String baseUnit() { + return unit; + } + + public String description() { + return description; + } } static class Counter extends NoOpMeter implements io.helidon.metrics.api.Counter { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 14a92b70730..3a0f8e67cb5 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -55,7 +55,7 @@ public Counter.Builder counterBuilder(String name) { } @Override - public DistributionSummary.Builder distributionSummaryBuilder(String name) { + public DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder) { // TODO return null; } diff --git a/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java index e4639415a96..a869784ec56 100644 --- a/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java +++ b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java @@ -100,7 +100,7 @@ void testFilteredMetersFetch() { @Test void testFilteredMetersWithNoMatches() { - Collection candidateCounters = + Collection candidateCounters = Metrics.globalRegistry() .meters(m -> m.id().name().equals("no such meter")); diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml index 38f2eb518ed..ed49517a6e2 100644 --- a/metrics/providers/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -39,6 +39,10 @@ io.micrometer micrometer-core + + io.micrometer + micrometer-registry-prometheus + org.junit.jupiter junit-jupiter-api diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java similarity index 56% rename from metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java index 1bac4a12f1e..1befca53fc9 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerTag.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java @@ -15,31 +15,34 @@ */ package io.helidon.metrics.micrometer; -import io.helidon.metrics.api.Tag; +import java.util.concurrent.TimeUnit; -class MicrometerTag implements Tag { +import io.micrometer.core.instrument.distribution.CountAtBucket; - static MicrometerTag of(String key, String value) { - return new MicrometerTag(io.micrometer.core.instrument.Tag.of(key, value)); - } +class MCountAtBucket implements io.helidon.metrics.api.CountAtBucket { - static MicrometerTag of(io.micrometer.core.instrument.Tag mTag) { - return of(mTag.getKey(), mTag.getValue()); + static MCountAtBucket of(CountAtBucket delegate) { + return new MCountAtBucket(delegate); } - private final io.micrometer.core.instrument.Tag delegate; + private final CountAtBucket delegate; - private MicrometerTag(io.micrometer.core.instrument.Tag delegate) { + private MCountAtBucket(CountAtBucket delegate) { this.delegate = delegate; } @Override - public String key() { - return delegate.getKey(); + public double bucket() { + return delegate.bucket(); + } + + @Override + public double bucket(TimeUnit unit) { + return delegate.bucket(unit); } @Override - public String value() { - return delegate.getValue(); + public double count() { + return delegate.count(); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java new file mode 100644 index 00000000000..94c64cc53ca --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; + +class MCounter extends MMeter implements io.helidon.metrics.api.Counter { + + static Builder builder(String name) { + return new Builder(name); + } + + static MCounter of(Counter counter) { + return new MCounter(counter); + } + + private MCounter(Counter delegate) { + super(delegate); + } + + @Override + public void increment() { + delegate().increment(); + } + + @Override + public void increment(double amount) { + delegate().increment(amount); + } + + @Override + public double count() { + return delegate().count(); + } + + static class Builder extends MMeter.Builder + implements io.helidon.metrics.api.Counter.Builder { + + private Builder(String name) { + super(name, Counter.builder(name)); + prep(delegate()::tags, + delegate()::description, + delegate()::baseUnit); + } + + @Override + MCounter register(MeterRegistry meterRegistry) { + return MCounter.of(delegate().register(meterRegistry)); + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java new file mode 100644 index 00000000000..6bd151c0d24 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; + +class MDistributionStatisticsConfig implements io.helidon.metrics.api.DistributionStatisticsConfig { + + static Builder builder() { + return new Builder(); + } + + private final DistributionStatisticConfig delegate; + + /** + * Creates a new config instance from a builder (which wraps a Micrometer config builder). + * + * @param builder builder which wraps the Micrometer config builder + */ + private MDistributionStatisticsConfig(Builder builder) { + delegate = builder.delegate.build(); + } + + /** + * Creates a new config instance, primarily from a merge operation. + * + * @param delegate pre-existing delegate + */ + private MDistributionStatisticsConfig(DistributionStatisticConfig delegate) { + this.delegate = delegate; + } + + @Override + public MDistributionStatisticsConfig merge(io.helidon.metrics.api.DistributionStatisticsConfig parent) { + DistributionStatisticConfig newDelegate = DistributionStatisticConfig.builder() + .percentilesHistogram( + chooseOpt(delegate.isPercentileHistogram(), + parent::isPercentileHistogram)) + .percentiles( + choose(delegate.getPercentiles(), + () -> Util.doubleArray(parent.percentiles()))) + .serviceLevelObjectives( + choose(delegate.getServiceLevelObjectiveBoundaries(), + () -> Util.doubleArray(parent.serviceLevelObjectiveBoundaries()))) + .percentilePrecision( + chooseOpt(delegate.getPercentilePrecision(), + parent::percentilePrecision)) + .minimumExpectedValue( + chooseOpt(delegate.getMinimumExpectedValueAsDouble(), + parent::minimumExpectedValue)) + .maximumExpectedValue( + chooseOpt(delegate.getMaximumExpectedValueAsDouble(), + parent::maximumExpectedValue)) + .expiry( + chooseOpt(delegate.getExpiry(), + parent::expiry)) + .bufferLength( + chooseOpt(delegate.getBufferLength(), + parent::bufferLength)) + .build(); + return new MDistributionStatisticsConfig(newDelegate); + } + + + + @Override + public Optional isPercentileHistogram() { + return Optional.ofNullable(delegate.isPercentileHistogram()); + } + + @Override + public Optional isPublishingPercentiles() { + return Optional.of(delegate.isPublishingPercentiles()); + } + + @Override + public Optional isPublishingHistogram() { + return Optional.of(delegate.isPublishingHistogram()); + } + + @Override + public Optional> percentiles() { + return Optional.ofNullable(Util.iterable(delegate.getPercentiles())); + } + + @Override + public Optional percentilePrecision() { + return Optional.ofNullable(delegate.getPercentilePrecision()); + } + + @Override + public Optional minimumExpectedValue() { + return Optional.ofNullable(delegate.getMinimumExpectedValueAsDouble()); + } + + @Override + public Optional maximumExpectedValue() { + return Optional.ofNullable(delegate.getMaximumExpectedValueAsDouble()); + } + + @Override + public Optional expiry() { + return Optional.ofNullable(delegate.getExpiry()); + } + + @Override + public Optional bufferLength() { + return Optional.ofNullable(delegate.getBufferLength()); + } + + @Override + public Optional> serviceLevelObjectiveBoundaries() { + return Optional.ofNullable(Util.iterable(delegate.getServiceLevelObjectiveBoundaries())); + } + + static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder { + + + private final DistributionStatisticConfig.Builder delegate; + + private Builder() { + delegate = DistributionStatisticConfig.builder(); + } + + @Override + public MDistributionStatisticsConfig build() { + return new MDistributionStatisticsConfig(this); + } + + @Override + public Builder expiry(Duration expiry) { + delegate.expiry(expiry); + return this; + } + + @Override + public Builder bufferLength(Integer bufferLength) { + delegate.bufferLength(bufferLength); + return this; + } + + @Override + public Builder percentilesHistogram(Boolean enabled) { + delegate.percentilesHistogram(enabled); + return this; + } + + @Override + public Builder minimumExpectedValue(Double min) { + delegate.minimumExpectedValue(min); + return this; + } + + @Override + public Builder maximumExpectedValue(Double max) { + delegate.maximumExpectedValue(max); + return this; + } + + @Override + public Builder percentiles(double... percentiles) { + delegate.percentiles(percentiles); + return this; + } + + @Override + public Builder percentiles(Iterable percentiles) { + delegate.percentiles(Util.doubleArray(percentiles)); + return this; + } + + @Override + public Builder percentilePrecision(Integer digitsOfPrecision) { + delegate.percentilePrecision(digitsOfPrecision); + return this; + } + + @Override + public Builder serviceLevelObjectives(double... slos) { + delegate.serviceLevelObjectives(slos); + return this; + } + + @Override + public Builder serviceLevelObjectives(Iterable slos) { + delegate.serviceLevelObjectives(Util.doubleArray(slos)); + return this; + } + + DistributionStatisticConfig.Builder delegate() { + return delegate; + } + } + + static T chooseOpt(T fromChild, Supplier> fromParent) { + return Objects.requireNonNullElseGet(fromChild, + () -> fromParent.get().orElse(null)); + } + + static T choose(T fromChild, Supplier fromParent) { + return Objects.requireNonNullElseGet(fromChild, fromParent); + } + + +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java new file mode 100644 index 00000000000..abd2b92909b --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; + +class MDistributionSummary extends MMeter implements io.helidon.metrics.api.DistributionSummary { + + static Builder builder(String name, + io.helidon.metrics.api.DistributionStatisticsConfig.Builder configBuilder) { + return new Builder(name, configBuilder); + } + + static MDistributionSummary of(DistributionSummary summary) { + return new MDistributionSummary(summary); + } + + private MDistributionSummary(DistributionSummary delegate) { + super(delegate); + } + + @Override + public void record(double amount) { + delegate().record(amount); + } + + @Override + public long count() { + return delegate().count(); + } + + @Override + public double totalAmount() { + return delegate().totalAmount(); + } + + @Override + public double mean() { + return delegate().mean(); + } + + @Override + public double max() { + return delegate().max(); + } + + static class Builder extends MMeter.Builder + implements io.helidon.metrics.api.DistributionSummary.Builder { + + private Builder(String name, io.helidon.metrics.api.DistributionStatisticsConfig.Builder configBuilder) { + super(name, DistributionSummary.builder(name)); + } + + @Override + public Builder scale(double scale) { + delegate().scale(scale); + return identity(); + } + + @Override + public Builder distributionStatisticsConfig( + io.helidon.metrics.api.DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + io.helidon.metrics.api.DistributionStatisticsConfig config = distributionStatisticsConfigBuilder.build(); + DistributionSummary.Builder delegate = delegate(); + + config.percentiles().ifPresent(p -> delegate.publishPercentiles(Util.doubleArray(p))); + config.percentilePrecision().ifPresent(delegate::percentilePrecision); + config.isPercentileHistogram().ifPresent(delegate::publishPercentileHistogram); + config.serviceLevelObjectiveBoundaries().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos))); + config.minimumExpectedValue().ifPresent(delegate::minimumExpectedValue); + config.maximumExpectedValue().ifPresent(delegate::maximumExpectedValue); + config.expiry().ifPresent(delegate::distributionStatisticExpiry); + config.bufferLength().ifPresent(delegate::distributionStatisticBufferLength); + + return identity(); + } + + @Override + MDistributionSummary register(MeterRegistry meterRegistry) { + return MDistributionSummary.of(delegate().register(meterRegistry)); + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java new file mode 100644 index 00000000000..02d5baa7cd7 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.function.ToDoubleFunction; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; + +class MGauge extends MMeter implements io.helidon.metrics.api.Gauge { + + static MGauge.Builder builder(String name, T stateObject, ToDoubleFunction fn) { + return new MGauge.Builder<>(name, stateObject, fn); + } + static MGauge of(Gauge gauge) { + return new MGauge(gauge); + } + + private MGauge(Gauge delegate) { + super(delegate); + } + + @Override + public double value() { + return delegate().value(); + } + + static class Builder extends MMeter.Builder, MGauge.Builder, MGauge> + implements io.helidon.metrics.api.Gauge.Builder { + + private Builder(String name, T stateObject, ToDoubleFunction fn) { + super(name, Gauge.builder(name, stateObject, fn)); + prep(delegate()::tags, + delegate()::description, + delegate()::baseUnit); + } + + @Override + MGauge register(MeterRegistry meterRegistry) { + return MGauge.of(delegate().register(meterRegistry)); + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java new file mode 100644 index 00000000000..9d18a6f6566 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.io.PrintStream; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.distribution.CountAtBucket; +import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import io.micrometer.core.instrument.distribution.ValueAtPercentile; + +class MHistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot { + + static MHistogramSnapshot of(HistogramSnapshot delegate) { + return new MHistogramSnapshot(delegate); + } + + private final HistogramSnapshot delegate; + + private MHistogramSnapshot(HistogramSnapshot delegate) { + this.delegate = delegate; + } + + @Override + public long count() { + return delegate.count(); + } + + @Override + public double total() { + return delegate.total(); + } + + @Override + public double total(TimeUnit timeUnit) { + return delegate.total(timeUnit); + } + + @Override + public double max() { + return delegate.max(); + } + + @Override + public double mean() { + return delegate.mean(); + } + + @Override + public double mean(TimeUnit timeUnit) { + return delegate.mean(timeUnit); + } + + @Override + public Iterable percentileValues() { + return () -> new Iterator<>() { + + private final ValueAtPercentile[] values = delegate.percentileValues(); + private int slot; + + @Override + public boolean hasNext() { + return slot < values.length; + } + + @Override + public io.helidon.metrics.api.ValueAtPercentile next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return MValueAtPercentile.of(values[slot++]); + } + }; + } + + @Override + public Iterable histogramCounts() { + return () -> new Iterator<>() { + + private final CountAtBucket[] counts = delegate.histogramCounts(); + private int slot; + + @Override + public boolean hasNext() { + return slot < counts.length; + } + + @Override + public io.helidon.metrics.api.CountAtBucket next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return MCountAtBucket.of(counts[slot++]); + } + }; + } + + @Override + public void outputSummary(PrintStream out, double scale) { + delegate.outputSummary(out, scale); + + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java new file mode 100644 index 00000000000..397d088231c --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Iterator; +import java.util.function.Function; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +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.Timer; + +/** + * Adapter to Micrometer meter for Helidon metrics. + */ +class MMeter implements io.helidon.metrics.api.Meter { + + static MMeter of(Meter meter) { + if (meter instanceof Counter counter) { + return MCounter.of(counter); + } + if (meter instanceof DistributionSummary summary) { + return MDistributionSummary.of(summary); + } + if (meter instanceof Gauge gauge) { + return MGauge.of(gauge); + } + if (meter instanceof Timer timer) { + return MTimer.of(timer); + } + throw new IllegalArgumentException("Unrecognized meter type " + meter.getClass().getName()); + } + + private final M delegate; + + protected MMeter(M delegate) { + this.delegate = delegate; + } + + @Override + public io.helidon.metrics.api.Meter.Id id() { + return io.helidon.metrics.api.Meter.Id.of(delegate.getId().getName(), + MTag.neutralTags(delegate.getId().getTags())); + } + + @Override + public String baseUnit() { + return delegate.getId().getBaseUnit(); + } + + @Override + public String description() { + return delegate.getId().getDescription(); + } + + @Override + public Type type() { + return io.helidon.metrics.api.Meter.Type.valueOf(delegate.getId() + .getType() + .name()); + } + + protected M delegate() { + return delegate; + } + + abstract static class Builder, HM extends io.helidon.metrics.api.Meter> + /* implements io.helidon.metrics.api.Meter.Builder */{ + + private final String name; + private final B delegate; + private Function, B> tagsSetter; + private Function descriptionSetter; + private Function baseUnitSetter; + + protected Builder(String name, B delegate) { + this.name = name; + this.delegate = delegate; + } + + protected void prep(Function, B> tagsSetter, + Function descriptionSetter, + Function baseUnitSetter) { + this.tagsSetter = tagsSetter; + this.descriptionSetter = descriptionSetter; + this.baseUnitSetter = baseUnitSetter; + } + + protected B delegate() { + return delegate; + } + +// @Override + public HB tags(Iterable tags) { + tagsSetter.apply(MTag.tags(tags)); + return identity(); + } + +// @Override + public HB description(String description) { + descriptionSetter.apply(description); + return identity(); + } + +// @Override + public HB baseUnit(String baseUnit) { + baseUnitSetter.apply(baseUnit); + return identity(); + } + + public HB identity() { + return (HB) this; + } + + abstract HM register(MeterRegistry meterRegistry); + +// String name() { +// return name; +// } +// +// Iterable tags() { +// return ; +// } +// +// @Override +// public String description() { +// return null; +// } +// +// @Override +// public String baseUnit() { +// return null; +// } + } + static class Id implements io.helidon.metrics.api.Meter.Id { + + static Id of(Meter.Id id) { + return new Id(id); + } + + private final Meter.Id delegate; + + private Id(Meter.Id delegate) { + this.delegate = delegate; + } + + @Override + public String name() { + return delegate.getName(); + } + + @Override + public Iterable tags() { + return new Iterable<>() { + + private final Iterator iter = delegate.getTags().iterator(); + @Override + public Iterator iterator() { + return new Iterator<>() { + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public io.helidon.metrics.api.Tag next() { + return MTag.of(iter.next()); + } + }; + } + }; + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java new file mode 100644 index 00000000000..6bb0caaffc8 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import io.helidon.common.config.Config; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; + +class MMeterRegistry implements io.helidon.metrics.api.MeterRegistry { + + static MMeterRegistry create(Config helidonConfig) { + return new MMeterRegistry(new PrometheusMeterRegistry(new PrometheusConfig() { + @Override + public String get(String key) { + return helidonConfig.get(key).asString().orElse(null); + } + })); + } + + private final MeterRegistry delegate; + + private MMeterRegistry(MeterRegistry delegate) { + this.delegate = delegate; + } + + @Override + public List meters() { + return delegate.getMeters() + .stream() + .map(MMeter::of) + .toList(); + } + + @Override + public Collection meters(Predicate filter) { + return delegate.getMeters() + .stream() + .map(MMeter::of) + .toList(); + } + + @Override + public > M getOrCreate(B builder) { + if (builder instanceof MCounter.Builder cBuilder) { + return (M) cBuilder.register(delegate); + } else if (builder instanceof MDistributionSummary.Builder sBuilder) { + return (M) sBuilder.delegate().register(delegate); + } else if (builder instanceof MGauge.Builder gBuilder) { + return (M) gBuilder.delegate().register(delegate); + } else if (builder instanceof MTimer.Builder tBuilder) { + return (M) tBuilder.delegate().register(delegate); + } else { + throw new IllegalArgumentException(String.format("Unexpected builder type %s, expected one of %s", + builder.getClass().getName(), + List.of(MCounter.Builder.class.getName(), + MDistributionSummary.Builder.class.getName(), + MGauge.Builder.class.getName(), + MTimer.Builder.class.getName()))); + } + } + + @Override + public Optional get(Class mClass, + String name, + Iterable tags) { + return Optional.empty(); + } + + @Override + public io.helidon.metrics.api.Meter remove(io.helidon.metrics.api.Meter meter) { + return null; + } + + @Override + public io.helidon.metrics.api.Meter remove(io.helidon.metrics.api.Meter.Id id) { + return null; + } + + @Override + public io.helidon.metrics.api.Meter remove(String name, + Iterable tags) { + return null; + } + + MeterRegistry delegate() { + return delegate; + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java new file mode 100644 index 00000000000..427435f3b84 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Iterator; + +import io.micrometer.core.instrument.Tag; + +class MTag implements io.helidon.metrics.api.Tag { + + /** + * Adapts an {@link java.lang.Iterable} of Micrometer tag to an iterable of Helidon tag. + * + * @param tags Micrometer tags + * @return Helidon tags + */ + static Iterable neutralTags(Iterable tags) { + return () -> new Iterator<>() { + + private final Iterator iter = tags.iterator(); + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public io.helidon.metrics.api.Tag next() { + return MTag.of(iter.next()); + } + }; + } + + static Iterable tags(Iterable tags) { + return () -> new Iterator<>() { + + private final Iterator iter = tags.iterator(); + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Tag next() { + io.helidon.metrics.api.Tag next = iter.next(); + return Tag.of(next.key(), next.value()); + } + }; + } + + /** + * Adapts a Micrometer tag to a Helidon tag. + * + * @param tag Micrometer tag + * @return Helidon tag + */ + static MTag of(Tag tag) { + return new MTag(tag); + } + + private final Tag delegate; + + private MTag(Tag delegate) { + this.delegate = delegate; + } + + @Override + public String key() { + return delegate.getKey(); + } + + @Override + public String value() { + return delegate.getValue(); + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java new file mode 100644 index 00000000000..1ff468af1a8 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import io.helidon.metrics.api.DistributionStatisticsConfig; +import io.helidon.metrics.api.HistogramSnapshot; + +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; + +class MTimer extends MMeter implements io.helidon.metrics.api.Timer { + + static MTimer of(Timer timer) { + return new MTimer(timer); + } + + private MTimer(Timer delegate) { + super(delegate); + } + + @Override + public HistogramSnapshot takeSnapshot() { + return MHistogramSnapshot.of(delegate().takeSnapshot()); + } + + @Override + public void record(long amount, TimeUnit unit) { + delegate().record(amount, unit); + } + + @Override + public void record(Duration duration) { + delegate().record(duration); + } + + @Override + public T record(Supplier f) { + return delegate().record(f); + } + + @Override + public T record(Callable f) throws Exception { + return delegate().recordCallable(f); + } + + @Override + public void record(Runnable f) { + delegate().record(f); + } + + @Override + public Runnable wrap(Runnable f) { + return delegate().wrap(f); + } + + @Override + public Callable wrap(Callable f) { + return delegate().wrap(f); + } + + @Override + public Supplier wrap(Supplier f) { + return delegate().wrap(f); + } + + @Override + public long count() { + return delegate().count(); + } + + @Override + public double totalTime(TimeUnit unit) { + return delegate().totalTime(unit); + } + + @Override + public double mean(TimeUnit unit) { + return delegate().mean(unit); + } + + @Override + public double max(TimeUnit unit) { + return delegate().max(unit); + } + + static class Builder extends MMeter.Builder + implements io.helidon.metrics.api.Timer.Builder { + + private Builder(String name) { + super(name, Timer.builder(name)); + } + + @Override + public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig( + DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + io.helidon.metrics.api.DistributionStatisticsConfig config = distributionStatisticsConfigBuilder.build(); + Timer.Builder delegate = delegate(); + + config.percentiles().ifPresent(p -> delegate.publishPercentiles(Util.doubleArray(p))); + config.percentilePrecision().ifPresent(delegate::percentilePrecision); + config.isPercentileHistogram().ifPresent(delegate::publishPercentileHistogram); + config.serviceLevelObjectiveBoundaries().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos))); + config.minimumExpectedValue().ifPresent(delegate::minimumExpectedValue); + config.maximumExpectedValue().ifPresent(delegate::maximumExpectedValue); + config.expiry().ifPresent(delegate::distributionStatisticExpiry); + config.bufferLength().ifPresent(delegate::distributionStatisticBufferLength); + + return identity(); + return null; + } + + @Override + MTimer register(MeterRegistry meterRegistry) { + return MTimer.of(delegate().register(meterRegistry)); + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java new file mode 100644 index 00000000000..37bb2b8e077 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.distribution.ValueAtPercentile; + +class MValueAtPercentile implements io.helidon.metrics.api.ValueAtPercentile { + + static MValueAtPercentile of(ValueAtPercentile delegate) { + return new MValueAtPercentile(delegate); + } + + private final ValueAtPercentile delegate; + + MValueAtPercentile(ValueAtPercentile delegate) { + this.delegate = delegate; + } + + @Override + public double percentile() { + return delegate.percentile(); + } + + @Override + public double value() { + return delegate.value(); + } + + @Override + public double value(TimeUnit unit) { + return delegate.value(unit); + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java new file mode 100644 index 00000000000..6f2cdae4760 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.lang.reflect.Constructor; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.prometheus.PrometheusConfig; +import io.micrometer.prometheus.PrometheusMeterRegistry; + +/** + * Provides the Micrometer meter registry to use as a delegate for the implementation of the Helidon metrics API. + */ +class MeterRegistryProvider { + + private static final String PROMETHEUS_CONFIG_CLASS_NAME = PrometheusConfig.class.getName(); + private static final String PROMETHEUS_METER_REGISTRY_CLASS_NAME = PrometheusMeterRegistry.class.getName(); + private static final MeterRegistry prometheusMeterRegistry; + + static { + try { + Class prometheusConfigClass = Class.forName(PROMETHEUS_CONFIG_CLASS_NAME); + Class prometheusMeterRegistryClass = Class.forName(PROMETHEUS_METER_REGISTRY_CLASS_NAME); + try { + Constructor ctor = prometheusMeterRegistryClass.getConstructor(PrometheusConfig.class); + prometheusMeterRegistry = (PrometheusMeterRegistry) ctor.newInstance() + } catch (NoSuchMethodException e) { + throw new RuntimeException("Found " + PrometheusMeterRegistry.class.getName() + + " but unable to locate the expected constructor", e); + } + } catch (ClassNotFoundException e) { + prometheusMeterRegistry = null; + } + } + static MeterRegistry meterRegistry() { + + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java index 15653bbfde4..2447264230a 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java @@ -15,50 +15,91 @@ */ package io.helidon.metrics.micrometer; +import java.util.function.ToDoubleFunction; + +import io.helidon.metrics.api.Clock; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.DistributionStatisticsConfig; +import io.helidon.metrics.api.DistributionSummary; +import io.helidon.metrics.api.Gauge; +import io.helidon.metrics.api.HistogramSnapshot; +import io.helidon.metrics.api.HistogramSupport; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +import io.micrometer.core.instrument.Metrics; + /** * Implementation of Helidon metrics based on Micrometer. */ -public class MicrometerMetricsProvider { /* implements MetricsFactory { +public class MicrometerMetricsProvider implements MetricsFactory { + @Override - public Tag tagOf(String key, String value) { - return MicrometerTag.of(key, value); + public MeterRegistry globalRegistry() { + return MMeterRegistry.create(Metrics.globalRegistry); + } + + @Override + public Counter.Builder counterBuilder(String name) { + return MCounter.builder(name); + } + + @Override + public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() { + return null; + } + + @Override + public DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder) { + return MDistributionSummary.builder(name, configBuilder); + } + + @Override + public Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn) { + return MGauge.builder(name, stateObject, fn); } @Override - public Tags tags_of(String key, String value) { - return MicrometerTags.of(key, value); + public HistogramSupport.Builder histogramSupportBuilder() { + return null; } @Override - public Tags tags_concat(Iterable tags, Iterable other) { - return MicrometerTags.concat(tags, other); + public Timer.Builder timerBuilder(String name) { + return Timer.builder(name); } @Override - public Tags tags_concat(Iterable tags, String... keyValues) { - return MicrometerTags.concat(tags, keyValues); + public Timer.Sample timerStart() { + return null; } @Override - public Tags tags_of(Iterable tags) { - return MicrometerTags.of(tags); + public Timer.Sample timerStart(MeterRegistry registry) { + return null; } @Override - public Tags tags_of(String... keyValues) { - return MicrometerTags.of(keyValues); + public Timer.Sample timerStart(Clock clock) { + return null; } @Override - public Tags tags_of(Tag... tags) { - return MicrometerTags.of(tags); + public Meter.Id idOf(String name, Iterable tags) { + return null; } @Override - public Tags tags_empty() { - return MicrometerTags.empty(); + public Tag tagOf(String key, String value) { + return null; } -*/ + @Override + public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { + return null; + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java new file mode 100644 index 00000000000..cacde371e64 --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +class Util { + + private Util() { + } + + static double[] doubleArray(Iterable iter) { + List values = new ArrayList<>(); + iter.forEach(values::add); + double[] result = new double[values.size()]; + for (int i = 0; i < values.size(); i++) { + result[i] = values.get(i); + } + return result; + } + + static double[] doubleArray(Optional> iter) { + return iter.map(Util::doubleArray).orElse(null); + } + + static Iterable iterable(double[] items) { + return items == null + ? null + : () -> new Iterator<>() { + + private int slot; + + @Override + public boolean hasNext() { + return slot < items.length; + } + + @Override + public Double next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return items[slot++]; + } + }; + } +} diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index d595ae1a793..5c8029e3d72 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -21,6 +21,7 @@ requires io.helidon.metrics.api; requires micrometer.core; + requires static micrometer.registry.prometheus; // provides io.helidon.metrics.api.MetricsFactory with MicrometerMetricsProvider; } \ No newline at end of file From 6ed24a222be64cd76482289d4d4909eea0fd5e4c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 7 Aug 2023 07:40:18 -0500 Subject: [PATCH 13/40] Fix API for remove to return Optional; more Micrometer implementation updates; still WIP--pushing for safe-keeping --- .../io/helidon/metrics/api/MeterRegistry.java | 14 +-- .../helidon/metrics/api/MetricsFactory.java | 2 +- .../io/helidon/metrics/api/NoOpMeter.java | 43 ++++++- .../java/io/helidon/metrics/api/Timer.java | 70 ++++++++++- .../metrics/micrometer/MCountAtBucket.java | 2 +- .../helidon/metrics/micrometer/MCounter.java | 4 +- .../MDistributionStatisticsConfig.java | 1 - .../micrometer/MDistributionSummary.java | 4 +- .../io/helidon/metrics/micrometer/MGauge.java | 4 +- .../micrometer/MHistogramSnapshot.java | 4 +- .../io/helidon/metrics/micrometer/MMeter.java | 30 ++--- .../metrics/micrometer/MMeterRegistry.java | 109 +++++++++++++++--- .../io/helidon/metrics/micrometer/MTag.java | 4 +- .../io/helidon/metrics/micrometer/MTimer.java | 74 ++++++++---- ...der.java => MicrometerMetricsFactory.java} | 4 +- .../io/helidon/metrics/micrometer/Util.java | 45 ++++++++ .../micrometer/src/main/java/module-info.java | 2 +- 17 files changed, 330 insertions(+), 86 deletions(-) rename metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/{MicrometerMetricsProvider.java => MicrometerMetricsFactory.java} (95%) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index d9e898b22f4..66b2602554e 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -110,31 +110,29 @@ default Optional getTimer(String name, Iterable tags) { */ Optional get(Class mClass, String name, Iterable tags); - - /** * Removes a previously-registered meter. * * @param meter the meter to remove - * @return the removed meter; null if the meter is not currently registered + * @return the removed meter; empty if the meter is not currently registered */ - Meter remove(Meter meter); + Optional remove(Meter meter); /** * Removes a previously-registered meter with the specified ID. * * @param id ID for the meter to remove - * @return the removed meter; null if the meter is not currently registered + * @return the removed meter; empty if the meter is not currently registered */ - Meter remove(Meter.Id id); + Optional remove(Meter.Id id); /** * Removes a previously-registered meter with the specified name and tags. * * @param name counter name * @param tags tags for further identifying the meter - * @return the removed meter; null if the specified name and tags does not correspond to a registered meter + * @return the removed meter; empty if the specified name and tags does not correspond to a registered meter */ - Meter remove(String name, + Optional remove(String name, Iterable tags); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index c0d3cef46ca..07a394385bc 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -28,7 +28,7 @@ * {@link io.helidon.metrics.api.Timer#start(io.helidon.metrics.api.MeterRegistry)} method. *

*

- * ALso, various static methods create or return previously-created instances. + * Also, various static methods create or return previously-created instances. *

*

* Note that this is not intended to be the interface which developers use to work with Helidon metrics. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 21e0fc8a283..46124474186 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -370,8 +370,47 @@ public Timer build() { } @Override - public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig( - DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + public Builder publishPercentiles(double... percentiles) { + return identity(); + } + + @Override + public Builder percentilePrecision(Integer digitsOfPrecision) { + return identity(); + } + + @Override + public Builder publishPercentileHistogram() { + return identity(); + } + + @Override + public Builder publishPercentileHistogram(Boolean enabled) { + return identity(); + } + + @Override + public Builder serviceLevelObjectives(Duration... slos) { + return identity(); + } + + @Override + public Builder minimumExpectedValue(Duration min) { + return identity(); + } + + @Override + public Builder maximumExpectedValue(Duration max) { + return identity(); + } + + @Override + public Builder distributionStatisticExpiry(Duration expiry) { + return identity(); + } + + @Override + public Builder distributionStatisticBufferLength(Integer bufferLength) { return identity(); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index 14d1b4f9127..b2378229552 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -192,12 +192,76 @@ interface Sample { interface Builder extends Meter.Builder { /** - * Configures the distribution statistics for the timer. + * Sets the percentiles to compute and publish (expressing, for example, the 95th percentile as 0.95). * - * @param distributionStatisticsConfigBuilder builder for the distribution statistics config + * @param percentiles percentiles to compute and publish * @return updated builder */ - Builder distributionStatisticsConfig(DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder); + Builder publishPercentiles(double... percentiles); + /** + * Sets the precision for computing histogram percentile approximations. + * + * @param digitsOfPrecision number of digits of precision + * @return updated builder + */ + Builder percentilePrecision(Integer digitsOfPrecision); + + /** + * Sets to add histogram buckets. + *

+ * Equivalent to {@code publishPercentilHistogram(true)}). + *

+ * + * @return updated builder + */ + Builder publishPercentileHistogram(); + + /** + * Sets whether to add histogram buckets. + * + * @param enabled true/false + * @return updated builder + */ + Builder publishPercentileHistogram(Boolean enabled); + + /** + * Sets the service level objectives, guaranteeing at least those buckets in the histogram. + * + * @param slos service-level objective bucket boundaries + * @return updated builder + */ + Builder serviceLevelObjectives(Duration... slos); + + /** + * Sets the minimum expected value the timer is expected to record. + * @param min minimum expected value + * @return updated builder + */ + Builder minimumExpectedValue(Duration min); + + /** + * Sets the maximum expected value the timer is expected to record. + * + * @param max maximum expected value + * @return updated builder + */ + Builder maximumExpectedValue(Duration max); + + /** + * Sets how long age-decayed samples are retained in ring buffers for use in the timer histograms. + * + * @param expiry amount of time to keep samples + * @return updated builder + */ + Builder distributionStatisticExpiry(Duration expiry); + + /** + * Sets the size of the ring buffer for retaining samples for histograms. + * + * @param bufferLength size of the ring buffer to use + * @return updated builder + */ + Builder distributionStatisticBufferLength(Integer bufferLength); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java index 1befca53fc9..70a8cfe91e6 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java @@ -21,7 +21,7 @@ class MCountAtBucket implements io.helidon.metrics.api.CountAtBucket { - static MCountAtBucket of(CountAtBucket delegate) { + static MCountAtBucket create(CountAtBucket delegate) { return new MCountAtBucket(delegate); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java index 94c64cc53ca..1c8119f3832 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java @@ -24,7 +24,7 @@ static Builder builder(String name) { return new Builder(name); } - static MCounter of(Counter counter) { + static MCounter create(Counter counter) { return new MCounter(counter); } @@ -59,7 +59,7 @@ private Builder(String name) { @Override MCounter register(MeterRegistry meterRegistry) { - return MCounter.of(delegate().register(meterRegistry)); + return MCounter.create(delegate().register(meterRegistry)); } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java index 6bd151c0d24..57e2670099f 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java @@ -133,7 +133,6 @@ public Optional> serviceLevelObjectiveBoundaries() { static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder { - private final DistributionStatisticConfig.Builder delegate; private Builder() { diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index abd2b92909b..16cb0e5f027 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -25,7 +25,7 @@ static Builder builder(String name, return new Builder(name, configBuilder); } - static MDistributionSummary of(DistributionSummary summary) { + static MDistributionSummary create(DistributionSummary summary) { return new MDistributionSummary(summary); } @@ -91,7 +91,7 @@ public Builder distributionStatisticsConfig( @Override MDistributionSummary register(MeterRegistry meterRegistry) { - return MDistributionSummary.of(delegate().register(meterRegistry)); + return MDistributionSummary.create(delegate().register(meterRegistry)); } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java index 02d5baa7cd7..245a5d80d6b 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java @@ -25,7 +25,7 @@ class MGauge extends MMeter implements io.helidon.metrics.api.Gauge { static MGauge.Builder builder(String name, T stateObject, ToDoubleFunction fn) { return new MGauge.Builder<>(name, stateObject, fn); } - static MGauge of(Gauge gauge) { + static MGauge create(Gauge gauge) { return new MGauge(gauge); } @@ -50,7 +50,7 @@ private Builder(String name, T stateObject, ToDoubleFunction fn) { @Override MGauge register(MeterRegistry meterRegistry) { - return MGauge.of(delegate().register(meterRegistry)); + return MGauge.create(delegate().register(meterRegistry)); } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java index 9d18a6f6566..3f6882d2314 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java @@ -26,7 +26,7 @@ class MHistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot { - static MHistogramSnapshot of(HistogramSnapshot delegate) { + static MHistogramSnapshot create(HistogramSnapshot delegate) { return new MHistogramSnapshot(delegate); } @@ -105,7 +105,7 @@ public io.helidon.metrics.api.CountAtBucket next() { if (!hasNext()) { throw new NoSuchElementException(); } - return MCountAtBucket.of(counts[slot++]); + return MCountAtBucket.create(counts[slot++]); } }; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index 397d088231c..dfa031b3197 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -31,18 +31,18 @@ */ class MMeter implements io.helidon.metrics.api.Meter { - static MMeter of(Meter meter) { + static MMeter create(Meter meter) { if (meter instanceof Counter counter) { - return MCounter.of(counter); + return MCounter.create(counter); } if (meter instanceof DistributionSummary summary) { - return MDistributionSummary.of(summary); + return MDistributionSummary.create(summary); } if (meter instanceof Gauge gauge) { - return MGauge.of(gauge); + return MGauge.create(gauge); } if (meter instanceof Timer timer) { - return MTimer.of(timer); + return MTimer.create(timer); } throw new IllegalArgumentException("Unrecognized meter type " + meter.getClass().getName()); } @@ -130,24 +130,8 @@ public HB identity() { abstract HM register(MeterRegistry meterRegistry); -// String name() { -// return name; -// } -// -// Iterable tags() { -// return ; -// } -// -// @Override -// public String description() { -// return null; -// } -// -// @Override -// public String baseUnit() { -// return null; -// } } + static class Id implements io.helidon.metrics.api.Meter.Id { static Id of(Meter.Id id) { @@ -180,7 +164,7 @@ public boolean hasNext() { @Override public io.helidon.metrics.api.Tag next() { - return MTag.of(iter.next()); + return MTag.create(iter.next()); } }; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 6bb0caaffc8..706cb723281 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -18,16 +18,27 @@ import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import io.helidon.common.config.Config; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.search.RequiredSearch; +import io.micrometer.core.instrument.search.Search; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; class MMeterRegistry implements io.helidon.metrics.api.MeterRegistry { + private static final System.Logger LOGGER = System.getLogger(MMeterRegistry.class.getName()); + static MMeterRegistry create(Config helidonConfig) { return new MMeterRegistry(new PrometheusMeterRegistry(new PrometheusConfig() { @Override @@ -39,23 +50,25 @@ public String get(String key) { private final MeterRegistry delegate; + private final ConcurrentHashMap meters = new ConcurrentHashMap<>(); + private MMeterRegistry(MeterRegistry delegate) { this.delegate = delegate; + delegate.config() + .onMeterAdded(this::recordAdd) + .onMeterRemoved(this::recordRemove); } @Override public List meters() { - return delegate.getMeters() - .stream() - .map(MMeter::of) - .toList(); + return meters.values().stream().toList(); } @Override public Collection meters(Predicate filter) { - return delegate.getMeters() + return meters.values() .stream() - .map(MMeter::of) + .filter(filter) .toList(); } @@ -84,26 +97,96 @@ B extends io.helidon.metrics.api.Meter.Builder> M getOrCreate(B builder) { public Optional get(Class mClass, String name, Iterable tags) { - return Optional.empty(); + + Search search = delegate().find(name) + .tags(Util.tags(tags)); + Meter match; + + if (io.helidon.metrics.api.Counter.class.isAssignableFrom(mClass)) { + match = search.counter(); + } else if (io.helidon.metrics.api.DistributionSummary.class.isAssignableFrom(mClass)) { + match = search.summary(); + } else if (io.helidon.metrics.api.Gauge.class.isAssignableFrom(mClass)) { + match = search.gauge(); + } else if (io.helidon.metrics.api.Timer.class.isAssignableFrom(mClass)) { + match = search.timer(); + } else { + throw new IllegalArgumentException( + String.format("Provided class %s is not recognized", mClass.getName())); + } + if (match == null) { + return Optional.empty(); + } + io.helidon.metrics.api.Meter neutralMeter = meters.get(match); + if (neutralMeter == null) { + LOGGER.log(System.Logger.Level.WARNING, String.format("Found no Helidon counterpart for Micrometer meter %s %s", + name, + Util.list(tags))); + return Optional.empty(); + } + if (mClass.isInstance(neutralMeter)) { + return Optional.of(mClass.cast(neutralMeter)); + } + throw new IllegalArgumentException( + String.format("Matching meter is of type %s but %s was requested", + match.getClass().getName(), + mClass.getName())); + } @Override - public io.helidon.metrics.api.Meter remove(io.helidon.metrics.api.Meter meter) { - return null; + public Optional remove(io.helidon.metrics.api.Meter meter) { + return remove(meter.id()); } @Override - public io.helidon.metrics.api.Meter remove(io.helidon.metrics.api.Meter.Id id) { - return null; + public Optional remove(io.helidon.metrics.api.Meter.Id id) { + Meter nativeMeter = delegate.find(id.name()) + .tags(Util.tags(id.tags())) + .meter(); + if (nativeMeter == null) { + return Optional.empty(); + } + return Optional.ofNullable(meters.remove(nativeMeter)); } @Override - public io.helidon.metrics.api.Meter remove(String name, + public Optional remove(String name, Iterable tags) { - return null; + Meter nativeMeter = delegate.remove(new Meter.Id(name, + Tags.of(Util.tags(tags)), + null, + null, + null)); + if (nativeMeter == null) { + return Optional.empty(); + } + return Optional.ofNullable(meters.remove(nativeMeter)); } MeterRegistry delegate() { return delegate; } + + private void recordAdd(Meter addedMeter) { + if (addedMeter instanceof Counter counter) { + meters.put(addedMeter, MCounter.create(counter)); + } else if (addedMeter instanceof DistributionSummary summary) { + meters.put(addedMeter, MDistributionSummary.create(summary)); + } else if (addedMeter instanceof Gauge gauge) { + meters.put(addedMeter, MGauge.create(gauge)); + } else if (addedMeter instanceof Timer timer) { + meters.put(addedMeter, MTimer.create(timer)); + } else { + LOGGER.log(System.Logger.Level.WARNING, + "Attempt to record addition of unrecognized meter type " + addedMeter.getClass().getName()); + } + } + + private void recordRemove(Meter removedMeter) { + io.helidon.metrics.api.Meter removedNeutralMeter = meters.remove(removedMeter); + if (removedNeutralMeter == null) { + LOGGER.log(System.Logger.Level.WARNING, "No matching neutral meter for implementation meter " + removedMeter); + } + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java index 427435f3b84..9f0fca51622 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java @@ -39,7 +39,7 @@ public boolean hasNext() { @Override public io.helidon.metrics.api.Tag next() { - return MTag.of(iter.next()); + return MTag.create(iter.next()); } }; } @@ -68,7 +68,7 @@ public Tag next() { * @param tag Micrometer tag * @return Helidon tag */ - static MTag of(Tag tag) { + static MTag create(Tag tag) { return new MTag(tag); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 1ff468af1a8..636de672b6b 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -16,21 +16,18 @@ package io.helidon.metrics.micrometer; import java.time.Duration; -import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import io.helidon.metrics.api.DistributionStatisticsConfig; import io.helidon.metrics.api.HistogramSnapshot; -import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; class MTimer extends MMeter implements io.helidon.metrics.api.Timer { - static MTimer of(Timer timer) { + static MTimer create(Timer timer) { return new MTimer(timer); } @@ -40,7 +37,7 @@ private MTimer(Timer delegate) { @Override public HistogramSnapshot takeSnapshot() { - return MHistogramSnapshot.of(delegate().takeSnapshot()); + return MHistogramSnapshot.create(delegate().takeSnapshot()); } @Override @@ -111,27 +108,62 @@ private Builder(String name) { } @Override - public io.helidon.metrics.api.Timer.Builder distributionStatisticsConfig( - DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { - io.helidon.metrics.api.DistributionStatisticsConfig config = distributionStatisticsConfigBuilder.build(); - Timer.Builder delegate = delegate(); - - config.percentiles().ifPresent(p -> delegate.publishPercentiles(Util.doubleArray(p))); - config.percentilePrecision().ifPresent(delegate::percentilePrecision); - config.isPercentileHistogram().ifPresent(delegate::publishPercentileHistogram); - config.serviceLevelObjectiveBoundaries().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos))); - config.minimumExpectedValue().ifPresent(delegate::minimumExpectedValue); - config.maximumExpectedValue().ifPresent(delegate::maximumExpectedValue); - config.expiry().ifPresent(delegate::distributionStatisticExpiry); - config.bufferLength().ifPresent(delegate::distributionStatisticBufferLength); + MTimer register(MeterRegistry meterRegistry) { + return MTimer.create(delegate().register(meterRegistry)); + } + @Override + public Builder publishPercentiles(double... percentiles) { + delegate().publishPercentiles(percentiles); return identity(); - return null; } @Override - MTimer register(MeterRegistry meterRegistry) { - return MTimer.of(delegate().register(meterRegistry)); + public Builder percentilePrecision(Integer digitsOfPrecision) { + delegate().percentilePrecision(digitsOfPrecision); + return identity(); + } + + @Override + public Builder publishPercentileHistogram() { + delegate().publishPercentileHistogram(); + return identity(); + } + + @Override + public Builder publishPercentileHistogram(Boolean enabled) { + delegate().publishPercentileHistogram(enabled); + return identity(); + } + + @Override + public Builder serviceLevelObjectives(Duration... slos) { + delegate().serviceLevelObjectives(slos); + return identity(); + } + + @Override + public Builder minimumExpectedValue(Duration min) { + delegate().minimumExpectedValue(min); + return identity(); + } + + @Override + public Builder maximumExpectedValue(Duration max) { + delegate().maximumExpectedValue(max); + return identity(); + } + + @Override + public Builder distributionStatisticExpiry(Duration expiry) { + delegate().distributionStatisticExpiry(expiry); + return identity(); + } + + @Override + public Builder distributionStatisticBufferLength(Integer bufferLength) { + delegate().distributionStatisticBufferLength(bufferLength); + return identity(); } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java similarity index 95% rename from metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index 2447264230a..047714bbc7a 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -33,9 +33,9 @@ import io.micrometer.core.instrument.Metrics; /** - * Implementation of Helidon metrics based on Micrometer. + * Implementation of the neutral Helidon metrics factory based on Micrometer. */ -public class MicrometerMetricsProvider implements MetricsFactory { +public class MicrometerMetricsFactory implements MetricsFactory { @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java index cacde371e64..01c117e121b 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java @@ -16,11 +16,14 @@ package io.helidon.metrics.micrometer; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Optional; +import io.micrometer.core.instrument.Tag; + class Util { private Util() { @@ -40,6 +43,12 @@ static double[] doubleArray(Optional> iter) { return iter.map(Util::doubleArray).orElse(null); } + static List list(Iterable iterable) { + List result = new ArrayList<>(); + iterable.forEach(result::add); + return result; + } + static Iterable iterable(double[] items) { return items == null ? null @@ -61,4 +70,40 @@ public Double next() { } }; } + + static Iterable neutralTags(Iterable tags) { + return () -> new Iterator<>() { + + private final Iterator tagsIter = tags.iterator(); + + @Override + public boolean hasNext() { + return tagsIter.hasNext(); + } + + @Override + public io.helidon.metrics.api.Tag next() { + Tag next = tagsIter.next(); + return io.helidon.metrics.api.Tag.of(next.getKey(), next.getValue()); + } + }; + } + + static Iterable tags(Iterable tags) { + return () -> new Iterator<>() { + + private final Iterator tagsIter = tags.iterator(); + + @Override + public boolean hasNext() { + return tagsIter.hasNext(); + } + + @Override + public Tag next() { + io.helidon.metrics.api.Tag next = tagsIter.next(); + return Tag.of(next.key(), next.value()); + } + }; + } } diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index 5c8029e3d72..4fb02b4dcda 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -23,5 +23,5 @@ requires micrometer.core; requires static micrometer.registry.prometheus; -// provides io.helidon.metrics.api.MetricsFactory with MicrometerMetricsProvider; +// provides io.helidon.metrics.api.MetricsFactory with MicrometerMetricsFactory; } \ No newline at end of file From 2aa467ddbb13813557ab352de1f1d42d34326585 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 8 Aug 2023 03:37:12 -0500 Subject: [PATCH 14/40] Further work on Micrometer impl; some changes to API as well --- .../metrics/api/DistributionSummary.java | 1 + .../helidon/metrics/api/HistogramSupport.java | 17 +-- .../java/io/helidon/metrics/api/Meter.java | 35 ----- .../helidon/metrics/api/MetricsFactory.java | 22 ++- .../io/helidon/metrics/api/NoOpMeter.java | 127 +++++++++++++++++- .../metrics/api/NoOpMeterRegistry.java | 10 +- .../metrics/api/NoOpMetricsFactory.java | 30 +++-- .../io/helidon/metrics/micrometer/MClock.java | 45 +++++++ .../io/helidon/metrics/micrometer/MMeter.java | 24 +--- .../metrics/micrometer/MMeterRegistry.java | 20 ++- .../io/helidon/metrics/micrometer/MTag.java | 9 ++ .../io/helidon/metrics/micrometer/MTimer.java | 46 +++++++ .../micrometer/MicrometerMetricsFactory.java | 53 +++++--- ... => MicrometerMetricsFactoryProvider.java} | 20 ++- .../io/helidon/metrics/micrometer/Util.java | 1 - .../micrometer/src/main/java/module-info.java | 5 +- 16 files changed, 334 insertions(+), 131 deletions(-) create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java rename metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/{MeterRegistryProvider.java => MicrometerMetricsFactoryProvider.java} (71%) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java index d585cb8df99..7ae423ea595 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -24,6 +24,7 @@ public interface DistributionSummary extends Meter { * Creates a builder for a new {@link io.helidon.metrics.api.DistributionSummary}. * * @param name name for the summary + * @param configBuilder distribution stats config for the summary * @return new builder */ static Builder builder(String name, diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java index 3fae2a46a86..bb171feec38 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSupport.java @@ -18,16 +18,7 @@ /** * Common behavior among meters which support histograms. */ -public interface HistogramSupport extends Meter { - - /** - * Creates a builder for a new {@link io.helidon.metrics.api.HistogramSupport} instance. - * - * @return new builder - */ - static Builder builder() { - return MetricsFactory.getInstance().histogramSupportBuilder(); - } +public interface HistogramSupport { /** * Returns a snapshot of the data in a histogram. @@ -35,10 +26,4 @@ static Builder builder() { * @return snapshot of the histogram */ HistogramSnapshot takeSnapshot(); - - /** - * Builder for a new {@link io.helidon.metrics.api.HistogramSupport}. - */ - interface Builder extends Meter.Builder { - } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index d80929ecd60..0fc8d52f749 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -20,41 +20,6 @@ */ public interface Meter extends Wrapped { -// /** -// * Behavior for adapters to builder in implementations. -// */ -// interface BuilderAdapter { -// -// /** -// * Returns the name assigned to the builder. -// * -// * @return name -// */ -// String name(); -// -// /** -// * Returns the tags assigned to the builder. -// * -// * @return tags -// */ -// Iterable tags(); -// -// /** -// * Returns the description assigned to the builder. -// * -// * @return description -// */ -// String description(); -// -// /** -// * Returns the unit assigned to the builder. -// * -// * @return unit -// */ -// String baseUnit(); -// -// } - /** * Common behavior of specific meter builders. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index 07a394385bc..a05ae8948f5 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -55,6 +55,14 @@ static MetricsFactory getInstance() { */ MeterRegistry globalRegistry(); + /** + * Returns the system {@link io.helidon.metrics.api.Clock} clock from the + * underlying metrics implementation. + * + * @return the system clock + */ + Clock clockSystem(); + /** * Creates a builder for a {@link io.helidon.metrics.api.Counter}. * @@ -74,6 +82,7 @@ static MetricsFactory getInstance() { * Creates a builder for a {@link io.helidon.metrics.api.DistributionSummary}. * * @param name name of the summary + * @param configBuilder distribution stats config the summary should use * @return summary builder */ DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder); @@ -89,12 +98,13 @@ static MetricsFactory getInstance() { */ Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn); - /** - * Creates a builder for a {@link io.helidon.metrics.api.HistogramSupport}. - * - * @return summary builder - */ - HistogramSupport.Builder histogramSupportBuilder(); + // TODO probably remove this but doublecheck +// /** +// * Creates a builder for a {@link io.helidon.metrics.api.HistogramSupport}. +// * +// * @return summary builder +// */ +// HistogramSupport.Builder histogramSupportBuilder(); /** * Creates a builder for a {@link io.helidon.metrics.api.Timer}. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 46124474186..0dfc4a365a0 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -285,7 +286,7 @@ public Builder scale(double scale) { @Override public Builder distributionStatisticsConfig( - DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { + io.helidon.metrics.api.DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) { return identity(); } } @@ -488,4 +489,128 @@ public double max(TimeUnit unit) { return 0; } } + + static class DistributionStatisticsConfig implements io.helidon.metrics.api.DistributionStatisticsConfig { + + static Builder builder() { + return new Builder(); + } + + static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder { + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig build() { + return new NoOpMeter.DistributionStatisticsConfig(this); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder expiry(Duration expiry) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder bufferLength(Integer bufferLength) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentilesHistogram(Boolean enabled) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder minimumExpectedValue(Double min) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder maximumExpectedValue(Double max) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentiles(double... percentiles) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentiles(Iterable percentiles) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentilePrecision(Integer digitsOfPrecision) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder serviceLevelObjectives(double... slos) { + return identity(); + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder serviceLevelObjectives(Iterable slos) { + return identity(); + } + } + + private DistributionStatisticsConfig(DistributionStatisticsConfig.Builder builder) { + } + + @Override + public io.helidon.metrics.api.DistributionStatisticsConfig merge( + io.helidon.metrics.api.DistributionStatisticsConfig parent) { + return builder().build(); + } + + @Override + public Optional isPercentileHistogram() { + return Optional.empty(); + } + + @Override + public Optional isPublishingPercentiles() { + return Optional.empty(); + } + + @Override + public Optional isPublishingHistogram() { + return Optional.empty(); + } + + @Override + public Optional> percentiles() { + return Optional.empty(); + } + + @Override + public Optional percentilePrecision() { + return Optional.empty(); + } + + @Override + public Optional minimumExpectedValue() { + return Optional.empty(); + } + + @Override + public Optional maximumExpectedValue() { + return Optional.empty(); + } + + @Override + public Optional expiry() { + return Optional.empty(); + } + + @Override + public Optional bufferLength() { + return Optional.empty(); + } + + @Override + public Optional> serviceLevelObjectiveBoundaries() { + return Optional.empty(); + } + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 397d1bff22d..03d89ec9006 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -51,17 +51,17 @@ public Collection meters(Predicate filter) { } @Override - public Meter remove(Meter.Id id) { - return meters.remove(id); + public Optional remove(Meter.Id id) { + return Optional.ofNullable(meters.remove(id)); } @Override - public Meter remove(Meter meter) { - return meters.remove(meter.id()); + public Optional remove(Meter meter) { + return Optional.ofNullable(meters.remove(meter.id())); } @Override - public Meter remove(String name, Iterable tags) { + public Optional remove(String name, Iterable tags) { return remove(NoOpMeter.Id.create(name, tags)); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 3a0f8e67cb5..a781584c3e5 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -15,7 +15,6 @@ */ package io.helidon.metrics.api; -import java.util.Set; import java.util.function.ToDoubleFunction; /** @@ -25,6 +24,18 @@ class NoOpMetricsFactory implements MetricsFactory { private final MeterRegistry meterRegistry = new NoOpMeterRegistry(); + private static final Clock SYSTEM_CLOCK = new Clock() { + @Override + public long wallTime() { + return System.currentTimeMillis(); + } + + @Override + public long monotonicTime() { + return System.nanoTime(); + } + }; + static NoOpMetricsFactory create() { return new NoOpMetricsFactory(); } @@ -35,8 +46,8 @@ public MeterRegistry globalRegistry() { } @Override - public Meter.Id idOf(String name) { - return idOf(name, Set.of()); + public Clock clockSystem() { + return SYSTEM_CLOCK; } @Override @@ -55,9 +66,10 @@ public Counter.Builder counterBuilder(String name) { } @Override - public DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder) { - // TODO - return null; + public DistributionSummary.Builder distributionSummaryBuilder(String name, + DistributionStatisticsConfig.Builder configBuilder) { + return NoOpMeter.DistributionSummary.builder(name) + .distributionStatisticsConfig(configBuilder); } @Override @@ -70,12 +82,6 @@ public Timer.Builder timerBuilder(String name) { return NoOpMeter.Timer.builder(name); } - @Override - public HistogramSupport.Builder histogramSupportBuilder() { - // TODO - return null; - } - @Override public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() { // TODO diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java new file mode 100644 index 00000000000..98b1749befa --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +/** + * + */ +class MClock implements io.helidon.metrics.api.Clock { + + static MClock create(io.micrometer.core.instrument.Clock delegate) { + return new MClock(delegate); + } + + private final io.micrometer.core.instrument.Clock delegate; + + private MClock(io.micrometer.core.instrument.Clock delegate) { + this.delegate = delegate; + } + @Override + public long wallTime() { + return delegate.wallTime(); + } + + @Override + public long monotonicTime() { + return delegate.monotonicTime(); + } + + io.micrometer.core.instrument.Clock delegate() { + return delegate; + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index dfa031b3197..8415aec8a13 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -18,35 +18,15 @@ import java.util.Iterator; import java.util.function.Function; -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.DistributionSummary; -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.Timer; /** * Adapter to Micrometer meter for Helidon metrics. */ class MMeter implements io.helidon.metrics.api.Meter { - static MMeter create(Meter meter) { - if (meter instanceof Counter counter) { - return MCounter.create(counter); - } - if (meter instanceof DistributionSummary summary) { - return MDistributionSummary.create(summary); - } - if (meter instanceof Gauge gauge) { - return MGauge.create(gauge); - } - if (meter instanceof Timer timer) { - return MTimer.create(timer); - } - throw new IllegalArgumentException("Unrecognized meter type " + meter.getClass().getName()); - } - private final M delegate; protected MMeter(M delegate) { @@ -81,7 +61,9 @@ protected M delegate() { } abstract static class Builder, HM extends io.helidon.metrics.api.Meter> - /* implements io.helidon.metrics.api.Meter.Builder */{ + // TODO check if we need the following or something similar + + /* implements io.helidon.metrics.api.Meter.Builder */ { private final String name; private final B delegate; diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 706cb723281..6705b89a93a 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -28,9 +28,7 @@ import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.search.RequiredSearch; import io.micrometer.core.instrument.search.Search; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; @@ -58,7 +56,7 @@ private MMeterRegistry(MeterRegistry delegate) { .onMeterAdded(this::recordAdd) .onMeterRemoved(this::recordRemove); } - + @Override public List meters() { return meters.values().stream().toList(); @@ -153,15 +151,15 @@ public Optional remove(io.helidon.metrics.api.Mete @Override public Optional remove(String name, Iterable tags) { - Meter nativeMeter = delegate.remove(new Meter.Id(name, - Tags.of(Util.tags(tags)), - null, - null, - null)); - if (nativeMeter == null) { - return Optional.empty(); + Meter nativeMeter = delegate.find(name) + .tags(Util.tags(tags)) + .meter(); + if (nativeMeter != null) { + io.helidon.metrics.api.Meter result = meters.get(nativeMeter); + delegate.remove(nativeMeter); + return Optional.of(result); } - return Optional.ofNullable(meters.remove(nativeMeter)); + return Optional.empty(); } MeterRegistry delegate() { diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java index 9f0fca51622..6239d88998a 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java @@ -18,6 +18,7 @@ import java.util.Iterator; import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; class MTag implements io.helidon.metrics.api.Tag { @@ -44,6 +45,10 @@ public io.helidon.metrics.api.Tag next() { }; } + static Tags mTags(Iterable tags) { + return Tags.of(tags(tags)); + } + static Iterable tags(Iterable tags) { return () -> new Iterator<>() { @@ -72,6 +77,10 @@ static MTag create(Tag tag) { return new MTag(tag); } + static MTag of(String key, String value) { + return MTag.create(Tag.of(key, value)); + } + private final Tag delegate; private MTag(Tag delegate) { diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 636de672b6b..87ca42f8be3 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -31,6 +31,52 @@ static MTimer create(Timer timer) { return new MTimer(timer); } + static io.helidon.metrics.api.Timer.Builder builder(String name) { + return new Builder(name); + } + + static Sample start() { + return Sample.create(Timer.start()); + } + + static Sample start(io.helidon.metrics.api.MeterRegistry meterRegistry) { + if (meterRegistry instanceof MeterRegistry mMeterRegistry) { + return Sample.create(Timer.start(mMeterRegistry)); + } + throw new IllegalArgumentException("Expected meter registry type " + MMeterRegistry.class.getName() + + " but was " + meterRegistry.getClass().getName()); + } + + static Sample start(io.helidon.metrics.api.Clock clock) { + if (clock instanceof MClock mClock) { + return Sample.create(Timer.start(mClock.delegate())); + } + throw new IllegalArgumentException("Expected clock type " + MClock.class.getName() + + " but was " + clock.getClass().getName()); + } + + static class Sample implements io.helidon.metrics.api.Timer.Sample { + + static Sample create(io.micrometer.core.instrument.Timer.Sample delegate) { + return new Sample(delegate); + } + + private final Timer.Sample delegate; + + private Sample(io.micrometer.core.instrument.Timer.Sample delegate) { + this.delegate = delegate; + } + + @Override + public long stop(io.helidon.metrics.api.Timer timer) { + if (timer instanceof MTimer mTimer) { + return delegate.stop(mTimer.delegate()); + } + throw new IllegalArgumentException("Expected timer type " + MTimer.class.getName() + + " but was " + timer.getClass().getName()); + } + } + private MTimer(Timer delegate) { super(delegate); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index 047714bbc7a..515f0720b0c 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -23,24 +23,47 @@ import io.helidon.metrics.api.DistributionSummary; import io.helidon.metrics.api.Gauge; import io.helidon.metrics.api.HistogramSnapshot; -import io.helidon.metrics.api.HistogramSupport; import io.helidon.metrics.api.Meter; import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.api.Tag; import io.helidon.metrics.api.Timer; -import io.micrometer.core.instrument.Metrics; - /** * Implementation of the neutral Helidon metrics factory based on Micrometer. */ -public class MicrometerMetricsFactory implements MetricsFactory { +class MicrometerMetricsFactory implements MetricsFactory { + static MicrometerMetricsFactory create(MetricsConfig metricsConfig) { + return new MicrometerMetricsFactory(metricsConfig); + } + + private MicrometerMetricsFactory(MetricsConfig metricsConfig) { + } @Override public MeterRegistry globalRegistry() { - return MMeterRegistry.create(Metrics.globalRegistry); + // TODO fix following null + return MMeterRegistry.create(null); + } + + @Override + public Clock clockSystem() { + return new Clock() { + + private final io.micrometer.core.instrument.Clock delegate = io.micrometer.core.instrument.Clock.SYSTEM; + + @Override + public long wallTime() { + return delegate.wallTime(); + } + + @Override + public long monotonicTime() { + return delegate.monotonicTime(); + } + }; } @Override @@ -50,11 +73,12 @@ public Counter.Builder counterBuilder(String name) { @Override public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() { - return null; + return MDistributionStatisticsConfig.builder(); } @Override - public DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder) { + public DistributionSummary.Builder distributionSummaryBuilder(String name, + DistributionStatisticsConfig.Builder configBuilder) { return MDistributionSummary.builder(name, configBuilder); } @@ -63,11 +87,6 @@ public Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFun return MGauge.builder(name, stateObject, fn); } - @Override - public HistogramSupport.Builder histogramSupportBuilder() { - return null; - } - @Override public Timer.Builder timerBuilder(String name) { return Timer.builder(name); @@ -75,17 +94,17 @@ public Timer.Builder timerBuilder(String name) { @Override public Timer.Sample timerStart() { - return null; + return MTimer.start(); } @Override public Timer.Sample timerStart(MeterRegistry registry) { - return null; + return MTimer.start(registry); } @Override public Timer.Sample timerStart(Clock clock) { - return null; + return MTimer.start(clock); } @Override @@ -95,11 +114,11 @@ public Meter.Id idOf(String name, Iterable tags) { @Override public Tag tagOf(String key, String value) { - return null; + return MTag.of(key, value); } @Override public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { - return null; + return MHistogramSnapshot.create(io.micrometer.core.instrument.distribution.HistogramSnapshot.empty(count, total, max)); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java similarity index 71% rename from metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java index 6f2cdae4760..373a2df53a6 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MeterRegistryProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java @@ -16,19 +16,25 @@ package io.helidon.metrics.micrometer; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.spi.MetricsFactoryProvider; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; /** * Provides the Micrometer meter registry to use as a delegate for the implementation of the Helidon metrics API. */ -class MeterRegistryProvider { +public class MicrometerMetricsFactoryProvider implements MetricsFactoryProvider { private static final String PROMETHEUS_CONFIG_CLASS_NAME = PrometheusConfig.class.getName(); private static final String PROMETHEUS_METER_REGISTRY_CLASS_NAME = PrometheusMeterRegistry.class.getName(); - private static final MeterRegistry prometheusMeterRegistry; + private static MeterRegistry prometheusMeterRegistry; static { try { @@ -36,16 +42,20 @@ class MeterRegistryProvider { Class prometheusMeterRegistryClass = Class.forName(PROMETHEUS_METER_REGISTRY_CLASS_NAME); try { Constructor ctor = prometheusMeterRegistryClass.getConstructor(PrometheusConfig.class); - prometheusMeterRegistry = (PrometheusMeterRegistry) ctor.newInstance() + prometheusMeterRegistry = (PrometheusMeterRegistry) ctor.newInstance(); + Metrics.globalRegistry.add(prometheusMeterRegistry); } catch (NoSuchMethodException e) { throw new RuntimeException("Found " + PrometheusMeterRegistry.class.getName() + " but unable to locate the expected constructor", e); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); } } catch (ClassNotFoundException e) { prometheusMeterRegistry = null; } } - static MeterRegistry meterRegistry() { - + @Override + public MetricsFactory create(MetricsConfig metricsConfig) { + return MicrometerMetricsFactory.create(metricsConfig); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java index 01c117e121b..96175c31d37 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java @@ -16,7 +16,6 @@ package io.helidon.metrics.micrometer; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index 4fb02b4dcda..cca63fcb3d4 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -14,6 +14,8 @@ * limitations under the License. */ +import io.helidon.metrics.micrometer.MicrometerMetricsFactoryProvider; + /** * Micrometer adapter for Helidon metrics API. */ @@ -22,6 +24,7 @@ requires io.helidon.metrics.api; requires micrometer.core; requires static micrometer.registry.prometheus; + requires io.helidon.common.config; -// provides io.helidon.metrics.api.MetricsFactory with MicrometerMetricsFactory; + provides io.helidon.metrics.spi.MetricsFactoryProvider with MicrometerMetricsFactoryProvider; } \ No newline at end of file From af124b101177399223098ebcd5f376a5dc025a3e Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 8 Aug 2023 10:13:56 -0500 Subject: [PATCH 15/40] Various clean-up, remove some TODOs --- .../java/io/helidon/metrics/api/Clock.java | 17 +++- .../java/io/helidon/metrics/api/Meter.java | 21 ----- .../metrics/api/MetricsConfigBlueprint.java | 16 ++-- .../helidon/metrics/api/MetricsFactory.java | 34 ++----- .../io/helidon/metrics/api/NoOpMeter.java | 89 ++++++++++++++++++- .../metrics/api/NoOpMeterRegistry.java | 23 +---- .../metrics/api/NoOpMetricsFactory.java | 17 ++-- .../helidon/metrics/micrometer/MCounter.java | 2 +- .../micrometer/MDistributionSummary.java | 2 +- .../io/helidon/metrics/micrometer/MGauge.java | 2 +- .../io/helidon/metrics/micrometer/MMeter.java | 9 +- .../metrics/micrometer/MMeterRegistry.java | 23 +++-- .../io/helidon/metrics/micrometer/MTimer.java | 2 +- .../micrometer/MicrometerMetricsFactory.java | 6 -- 14 files changed, 144 insertions(+), 119 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java index 9667a819d3c..77de260b350 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java @@ -20,11 +20,24 @@ */ public interface Clock extends Wrapped { + /** + * Returns the system clock for the Helidon metrics implementation. + *

+ * The system clock methods are functionally equivalent to {@link System#currentTimeMillis()} + * and {@link System#nanoTime()}. + *

+ * + * @return the system clock + */ + static Clock system() { + return MetricsFactoryManager.getInstance().clockSystem(); + } + /** * Returns the current wall time in milliseconds since the epoch. * *

- * Typically equivalent to System.currentTimeMillis. Should not be used to determine durations. + * Typically equivalent to {@link System#currentTimeMillis()}. Should not be used to determine durations. * For that use {@link #monotonicTime()} instead. *

* @@ -38,7 +51,7 @@ public interface Clock extends Wrapped { *

* The value is only meaningful when compared with another value returned from this method to determine the elapsed time * for an operation. The difference between two samples will have a unit of nanoseconds. The returned value is - * typically equivalent to System.nanoTime. + * typically equivalent to {@link System#nanoTime()}. *

* * @return monotonic time in nanoseconds diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index 0fc8d52f749..422211ba0c4 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -66,27 +66,6 @@ default B identity() { */ interface Id { - /** - * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name. - * - * @param name name for the ID - * @return new meter ID - */ - static Id of(String name) { - return MetricsFactoryManager.getInstance().idOf(name); - } - - /** - * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name and tags. - * - * @param name name for the ID - * @param tags tags for the ID - * @return new meter ID - */ - static Id of(String name, Iterable tags) { - return MetricsFactoryManager.getInstance().idOf(name, tags); - } - /** * Returns the meter name. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 0c5f1119d02..5c587452dc9 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.api; +import java.util.List; import java.util.Optional; import io.helidon.builder.api.Prototype; @@ -64,14 +65,13 @@ interface MetricsConfigBlueprint { @ConfiguredOption(key = KeyPerformanceIndicatorMetricsConfigBlueprint.KEY_PERFORMANCE_INDICATORS_CONFIG_KEY) Optional keyPerformanceIndicatorMetricsConfig(); -// TODO fix mapping -// /** -// * Global tags. -// * -// * @return name/value pairs for global tags -// */ -// @ConfiguredOption(key = GLOBAL_TAGS_CONFIG_KEY) -// Optional> globalTags(); + /** + * Global tags. + * + * @return name/value pairs for global tags + */ + @ConfiguredOption(key = GLOBAL_TAGS_CONFIG_KEY) + List globalTags(); @ConfiguredOption(key = APP_TAG_CONFIG_KEY) Optional appTagValue(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index a05ae8948f5..c78d488c483 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -15,7 +15,6 @@ */ package io.helidon.metrics.api; -import java.util.Set; import java.util.function.ToDoubleFunction; /** @@ -44,6 +43,11 @@ */ public interface MetricsFactory { + /** + * Returns the highest-weight implementation of the factory available at runtime. + * + * @return highest-weight metrics factory + */ static MetricsFactory getInstance() { return MetricsFactoryManager.getInstance(); } @@ -56,7 +60,7 @@ static MetricsFactory getInstance() { MeterRegistry globalRegistry(); /** - * Returns the system {@link io.helidon.metrics.api.Clock} clock from the + * Returns the system {@link io.helidon.metrics.api.Clock} from the * underlying metrics implementation. * * @return the system clock @@ -98,14 +102,6 @@ static MetricsFactory getInstance() { */ Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn); - // TODO probably remove this but doublecheck -// /** -// * Creates a builder for a {@link io.helidon.metrics.api.HistogramSupport}. -// * -// * @return summary builder -// */ -// HistogramSupport.Builder histogramSupportBuilder(); - /** * Creates a builder for a {@link io.helidon.metrics.api.Timer}. * @@ -140,24 +136,6 @@ static MetricsFactory getInstance() { */ Timer.Sample timerStart(Clock clock); - /** - * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name and tags. - * @param name name to use in the ID - * @param tags tags to use in the ID - * @return new meter ID - */ - Meter.Id idOf(String name, Iterable tags); - - /** - * Creates a {@link io.helidon.metrics.api.Meter.Id} from the specified name. - * - * @param name name to use in the ID - * @return new meter ID - */ - default Meter.Id idOf(String name) { - return idOf(name, Set.of()); - } - /** * Creates a {@link io.helidon.metrics.api.Tag} from the specified key and value. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 0dfc4a365a0..f2214920cd8 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.api; +import java.io.PrintStream; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -23,6 +24,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -40,7 +42,6 @@ class NoOpMeter implements Meter { private final Type type; static class Id implements Meter.Id { - static Id create(String name, Iterable tags) { return new Id(name, tags); } @@ -324,6 +325,67 @@ public double max() { } } + static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot { + + private final long count; + private final double total; + private final double max; + + static HistogramSnapshot empty(long count, double total, double max) { + return new HistogramSnapshot(count, total, max); + } + + private HistogramSnapshot(long count, double total, double max) { + this.count = count; + this.total = total; + this.max = max; + } + + @Override + public long count() { + return count; + } + + @Override + public double total() { + return total; + } + + @Override + public double total(TimeUnit timeUnit) { + return timeUnit.convert((long) total, TimeUnit.NANOSECONDS); + } + + @Override + public double max() { + return max; + } + + @Override + public double mean() { + return total / count; + } + + @Override + public double mean(TimeUnit timeUnit) { + return timeUnit.convert((long) mean(), TimeUnit.NANOSECONDS); + } + + @Override + public Iterable percentileValues() { + return Set.of(); + } + + @Override + public Iterable histogramCounts() { + return Set.of(); + } + + @Override + public void outputSummary(PrintStream out, double scale) { + } + } + static class Gauge extends NoOpMeter implements io.helidon.metrics.api.Gauge { static Builder builder(String name, T stateObject, ToDoubleFunction fn) { @@ -359,6 +421,29 @@ public double value() { static class Timer extends NoOpMeter implements io.helidon.metrics.api.Timer { + static class Sample implements io.helidon.metrics.api.Timer.Sample { + + private Sample() { + } + + @Override + public long stop(io.helidon.metrics.api.Timer timer) { + return 0; + } + } + + static Sample start() { + return new Sample(); + } + + static Sample start(MeterRegistry meterRegistry) { + return new Sample(); + } + + static Sample start(Clock clock) { + return new Sample(); + } + static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.Timer.Builder { private Builder(String name) { @@ -425,7 +510,7 @@ private Timer(Builder builder) { } @Override - public HistogramSnapshot takeSnapshot() { + public io.helidon.metrics.api.HistogramSnapshot takeSnapshot() { return null; } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 03d89ec9006..255d43beb9b 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -84,26 +84,11 @@ private Optional find(Meter.Id id, Class mClass) { private > M findOrRegister(Meter.Id id, B builder) { NoOpMeter.Builder noOpBuilder = (NoOpMeter.Builder) builder; + // The following cast will always succeed if we create the meter by invoking the builder, + // it will success if we retrieved a previously-registered meter of a compatible type, + // and it will (correctly) fail if we found a previously-registered meter of an incompatible + // type compared to what the caller requested. return (M) meters.computeIfAbsent(id, thidId -> noOpBuilder.build()); } -// TODO -// private M findOrRegister(Meter.Id id, Class mClass, Supplier meterSupplier) { -// // This next step is atomic because we are using a ConcurrentHashMap. -// Meter result = meters.computeIfAbsent(id, -// theId -> meterSupplier.get()); -// -// // Check the type in case we retrieved a previously-registered meter with the specified ID. The type will always -// // be correct if we ran the supplier, in which this test is unneeded by mostly harmless. -// // We could just attempt the cast and let Java throw a class cast exception itself if needed, but this is nicer. -// if (!mClass.isInstance(result)) { -// throw new IllegalArgumentException( -// String.format("Found previously-registered meter with ID %s of type %s when expecting %s", -// id, -// result.getClass().getName(), -// mClass.getName())); -// } -// -// return mClass.cast(meters.put(id, meterSupplier.get())); -// } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index a781584c3e5..c86ea44eb30 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -50,11 +50,6 @@ public Clock clockSystem() { return SYSTEM_CLOCK; } - @Override - public Meter.Id idOf(String name, Iterable tags) { - return NoOpMeter.Id.create(name, tags); - } - @Override public Tag tagOf(String key, String value) { return new NoOpTag(key, value); @@ -84,28 +79,26 @@ public Timer.Builder timerBuilder(String name) { @Override public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() { - // TODO - return null; + return NoOpMeter.DistributionStatisticsConfig.builder(); } - // TODO fix remaining null returns @Override public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { - return null; + return NoOpMeter.HistogramSnapshot.empty(count, total, max); } @Override public Timer.Sample timerStart() { - return null; + return NoOpMeter.Timer.start(); } @Override public Timer.Sample timerStart(MeterRegistry registry) { - return null; + return NoOpMeter.Timer.start(registry); } @Override public Timer.Sample timerStart(Clock clock) { - return null; + return NoOpMeter.Timer.start(clock); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java index 1c8119f3832..d6057e7640a 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java @@ -51,7 +51,7 @@ static class Builder extends MMeter.Builder implements io.helidon.metrics.api.Counter.Builder { private Builder(String name) { - super(name, Counter.builder(name)); + super(Counter.builder(name)); prep(delegate()::tags, delegate()::description, delegate()::baseUnit); diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index 16cb0e5f027..e9fc7243c81 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -62,7 +62,7 @@ static class Builder extends MMeter.Builder extends MMeter.Builder, MGauge.Builder< implements io.helidon.metrics.api.Gauge.Builder { private Builder(String name, T stateObject, ToDoubleFunction fn) { - super(name, Gauge.builder(name, stateObject, fn)); + super(Gauge.builder(name, stateObject, fn)); prep(delegate()::tags, delegate()::description, delegate()::baseUnit); diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index 8415aec8a13..5f135127d3d 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -28,15 +28,16 @@ class MMeter implements io.helidon.metrics.api.Meter { private final M delegate; + private final io.helidon.metrics.api.Meter.Id id; protected MMeter(M delegate) { this.delegate = delegate; + id = Id.of(delegate.getId()); } @Override public io.helidon.metrics.api.Meter.Id id() { - return io.helidon.metrics.api.Meter.Id.of(delegate.getId().getName(), - MTag.neutralTags(delegate.getId().getTags())); + return id; } @Override @@ -65,14 +66,12 @@ abstract static class Builder, HM extends io.he /* implements io.helidon.metrics.api.Meter.Builder */ { - private final String name; private final B delegate; private Function, B> tagsSetter; private Function descriptionSetter; private Function baseUnitSetter; - protected Builder(String name, B delegate) { - this.name = name; + protected Builder(B delegate) { this.delegate = delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 6705b89a93a..0dc35ea6318 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -28,6 +28,7 @@ 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.Timer; import io.micrometer.core.instrument.search.Search; import io.micrometer.prometheus.PrometheusConfig; @@ -139,20 +140,22 @@ public Optional remove(io.helidon.metrics.api.Mete @Override public Optional remove(io.helidon.metrics.api.Meter.Id id) { - Meter nativeMeter = delegate.find(id.name()) - .tags(Util.tags(id.tags())) - .meter(); - if (nativeMeter == null) { - return Optional.empty(); - } - return Optional.ofNullable(meters.remove(nativeMeter)); + return internalRemove(id.name(), Util.tags(id.tags())); } @Override public Optional remove(String name, Iterable tags) { + return internalRemove(name, Util.tags(tags)); + } + MeterRegistry delegate() { + return delegate; + } + + private Optional internalRemove(String name, + Iterable tags) { Meter nativeMeter = delegate.find(name) - .tags(Util.tags(tags)) + .tags(tags) .meter(); if (nativeMeter != null) { io.helidon.metrics.api.Meter result = meters.get(nativeMeter); @@ -162,10 +165,6 @@ public Optional remove(String name, return Optional.empty(); } - MeterRegistry delegate() { - return delegate; - } - private void recordAdd(Meter addedMeter) { if (addedMeter instanceof Counter counter) { meters.put(addedMeter, MCounter.create(counter)); diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 87ca42f8be3..1f29c4cef00 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -150,7 +150,7 @@ static class Builder extends MMeter.Builder tags) { - return null; - } - @Override public Tag tagOf(String key, String value) { return MTag.of(key, value); From c79d16509b17296a956507af71a1a74d75258d75 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 10 Aug 2023 03:22:36 -0500 Subject: [PATCH 16/40] Some tests for the Micrometer implementation; working on exposing config from MetricsConfig for use by Micrometer meter registry --- .../java/io/helidon/metrics/api/Metrics.java | 11 +++ .../metrics/api/MetricsConfigBlueprint.java | 13 +++- .../helidon/metrics/api/MetricsFactory.java | 8 +++ .../metrics/api/NoOpMeterRegistry.java | 26 +++---- .../metrics/api/NoOpMetricsFactory.java | 5 ++ .../io/helidon/metrics/api/SimpleApiTest.java | 67 ++----------------- .../helidon/metrics/micrometer/MCounter.java | 11 +-- .../micrometer/MDistributionSummary.java | 10 +-- .../io/helidon/metrics/micrometer/MGauge.java | 10 +-- .../io/helidon/metrics/micrometer/MMeter.java | 11 +-- .../metrics/micrometer/MMeterRegistry.java | 38 ++++++++--- .../io/helidon/metrics/micrometer/MTimer.java | 9 +-- .../micrometer/MicrometerMetricsFactory.java | 38 ++++++++++- .../MicrometerMetricsFactoryProvider.java | 53 ++++++++------- .../micrometer/SimpleMeterRegistryTests.java | 59 ++++++++++++++++ 15 files changed, 223 insertions(+), 146 deletions(-) create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index efa7eccca21..3fe86c2a593 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -35,6 +35,17 @@ static MeterRegistry globalRegistry() { return MetricsFactory.getInstance().globalRegistry(); } + /** + * Creates a meter registry, not added to the global registry, based on + * the provide metrics config. + * + * @param metricsConfig metrics config + * @return new meter registry + */ + static MeterRegistry createMeterRegistry(MetricsConfig metricsConfig) { + return MetricsFactory.getInstance().createMeterRegistry(metricsConfig); + } + /** * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new * one using the provided builder. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 5c587452dc9..0f7c1c8226f 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -30,8 +30,19 @@ @ConfigBean() @Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY) @Prototype.Blueprint(decorator = MetricsConfigBlueprint.BuilderDecorator.class) +@Prototype.CustomMethods(MetricsConfigBlueprint.CustomMethods.class) interface MetricsConfigBlueprint { + class CustomMethods { + + @Prototype.PrototypeMethod + static Optional lookupConfig(MetricsConfig metricsConfig, String key) { + return Optional.empty(); + } + + private CustomMethods() { + } + } /** * The config key containing settings for all of metrics. */ @@ -47,8 +58,6 @@ interface MetricsConfigBlueprint { */ String APP_TAG_CONFIG_KEY = "appName"; - - /** * Whether metrics functionality is enabled. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index c78d488c483..faca7a9cda6 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -59,6 +59,14 @@ static MetricsFactory getInstance() { */ MeterRegistry globalRegistry(); + /** + * Creates a new {@link MeterRegistry} using the provided metrics config. + * + * @param metricsConfig metrics configuration which influences the new registry + * @return new meter registry + */ + MeterRegistry createMeterRegistry(MetricsConfig metricsConfig); + /** * Returns the system {@link io.helidon.metrics.api.Clock} from the * underlying metrics implementation. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 255d43beb9b..6d43bdd28be 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -15,17 +15,21 @@ */ package io.helidon.metrics.api; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; /** * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}. + *

+ * Note that the no-op meter registry implement does not actually + * store meters or their IDs, in line with the documented behavior of disabled metrics. + *

*/ class NoOpMeterRegistry implements MeterRegistry { @@ -40,24 +44,17 @@ public List meters() { @Override public Collection meters(Predicate filter) { - List result = new ArrayList<>(); - meters.values() - .forEach(m -> { - if (filter.test(m)) { - result.add(m); - } - }); - return result; + return Set.of(); } @Override public Optional remove(Meter.Id id) { - return Optional.ofNullable(meters.remove(id)); + return Optional.empty(); } @Override public Optional remove(Meter meter) { - return Optional.ofNullable(meters.remove(meter.id())); + return Optional.empty(); } @Override @@ -67,7 +64,7 @@ public Optional remove(String name, Iterable tags) { @Override public Optional get(Class mClass, String name, Iterable tags) { - return Optional.ofNullable(mClass.cast(meters.get(NoOpMeter.Id.create(name, tags)))); + return Optional.empty(); } @Override @@ -79,7 +76,7 @@ public > M getOrCreate(B builder) } private Optional find(Meter.Id id, Class mClass) { - return Optional.ofNullable(mClass.cast(meters.get(id))); + return Optional.empty(); } private > M findOrRegister(Meter.Id id, B builder) { @@ -88,7 +85,6 @@ private > M findOrRegister(Meter. // it will success if we retrieved a previously-registered meter of a compatible type, // and it will (correctly) fail if we found a previously-registered meter of an incompatible // type compared to what the caller requested. - return (M) meters.computeIfAbsent(id, - thidId -> noOpBuilder.build()); + return (M) noOpBuilder.build(); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index c86ea44eb30..94897421042 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -45,6 +45,11 @@ public MeterRegistry globalRegistry() { return meterRegistry; } + @Override + public MeterRegistry createMeterRegistry(MetricsConfig metricsConfig) { + return new NoOpMeterRegistry(); + } + @Override public Clock clockSystem() { return SYSTEM_CLOCK; diff --git a/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java index a869784ec56..0c7d8b7f7bd 100644 --- a/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java +++ b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java @@ -30,6 +30,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; @@ -58,72 +59,12 @@ static void prep() { void testNoOpRegistrations() { Optional fetchedCounter = Metrics.getCounter("counter1"); - assertThat("Fetched counter 1", - fetchedCounter.map(Counter::description), - OptionalMatcher.optionalValue(is(COUNTER_1_DESC))); - + assertThat("Fetched counter 1", fetchedCounter, OptionalMatcher.optionalEmpty()); fetchedCounter = Metrics.getCounter("counter2", Set.of()); - assertThat("Fetched counter 2", - fetchedCounter.map(Meter::description), - OptionalMatcher.optionalEmpty()); + assertThat("Fetched counter 2", fetchedCounter, OptionalMatcher.optionalEmpty()); Optional fetchedTimer = Metrics.getTimer("timer1", Metrics.tags("t1", "v1", "t2", "v2")); - assertThat("Fetched timer", - fetchedTimer.map(Meter::baseUnit), - OptionalMatcher.optionalEmpty()); - - } - - @Test - void testAllMetersFetch() { - Meter meter = Metrics.globalRegistry() - .meters() - .stream() - .filter(m -> m.id().name().equals("counter1")) - .findFirst() - .orElse(null); - - assertThat("Counter1 via meters()", meter, sameInstance(counter1)); - } - - @Test - void testFilteredMetersFetch() { - List candidateCounters = new ArrayList<>(Metrics.globalRegistry() - .meters(m -> m.id().name().equals("counter1"))); - - assertThat("Results", candidateCounters, hasSize(1)); - assertThat("Single result", candidateCounters.get(0), instanceOf(Counter.class)); - assertThat("Result name", candidateCounters.get(0).id().name(), is(equalTo("counter1"))); - assertThat("Result", candidateCounters.get(0), sameInstance(counter1)); - } - - @Test - void testFilteredMetersWithNoMatches() { - Collection candidateCounters = - Metrics.globalRegistry() - .meters(m -> m.id().name().equals("no such meter")); - - assertThat("Results", candidateCounters, hasSize(0)); - } - - @Test - void testRemoval() { - MeterRegistry reg = Metrics.globalRegistry(); - - assertThat("Precheck of test counter", - Metrics.getCounter("doomedCounter"), - OptionalMatcher.optionalEmpty()); - - reg.getOrCreate(Counter.builder("doomedCounter") - .description("doomed counter") - ); - - reg.remove("doomedCounter", Set.of()); - - assertThat("Post-check of doomed counter", - Metrics.getCounter("doomedCounter"), - OptionalMatcher.optionalEmpty()); - + assertThat("Fetched timer", fetchedTimer, OptionalMatcher.optionalEmpty()); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java index d6057e7640a..620075daf1b 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java @@ -16,7 +16,6 @@ package io.helidon.metrics.micrometer; import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; class MCounter extends MMeter implements io.helidon.metrics.api.Counter { @@ -57,9 +56,11 @@ private Builder(String name) { delegate()::baseUnit); } - @Override - MCounter register(MeterRegistry meterRegistry) { - return MCounter.create(delegate().register(meterRegistry)); - } + // TODO remove if truly not used +// @Override +// MCounter register(MMeterRegistry mMeterRegistry) { +// +// return MCounter.create(delegate().register(mMeterRegistry)); +// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index e9fc7243c81..db59064416f 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -16,7 +16,6 @@ package io.helidon.metrics.micrometer; import io.micrometer.core.instrument.DistributionSummary; -import io.micrometer.core.instrument.MeterRegistry; class MDistributionSummary extends MMeter implements io.helidon.metrics.api.DistributionSummary { @@ -89,9 +88,10 @@ public Builder distributionStatisticsConfig( return identity(); } - @Override - MDistributionSummary register(MeterRegistry meterRegistry) { - return MDistributionSummary.create(delegate().register(meterRegistry)); - } + // TODO remove if not used +// @Override +// MDistributionSummary register(MeterRegistry meterRegistry) { +// return MDistributionSummary.create(delegate().register(meterRegistry)); +// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java index 559b1835360..08400ebf5a7 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java @@ -18,7 +18,6 @@ import java.util.function.ToDoubleFunction; import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; class MGauge extends MMeter implements io.helidon.metrics.api.Gauge { @@ -48,9 +47,10 @@ private Builder(String name, T stateObject, ToDoubleFunction fn) { delegate()::baseUnit); } - @Override - MGauge register(MeterRegistry meterRegistry) { - return MGauge.create(delegate().register(meterRegistry)); - } + // TODO remove if not used +// @Override +// MGauge register(MeterRegistry meterRegistry) { +// return MGauge.create(delegate().register(meterRegistry)); +// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index 5f135127d3d..a4c5dcab543 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -19,7 +19,6 @@ import java.util.function.Function; import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; /** @@ -61,10 +60,7 @@ protected M delegate() { return delegate; } - abstract static class Builder, HM extends io.helidon.metrics.api.Meter> - // TODO check if we need the following or something similar - - /* implements io.helidon.metrics.api.Meter.Builder */ { + abstract static class Builder, HM extends io.helidon.metrics.api.Meter> { private final B delegate; private Function, B> tagsSetter; @@ -87,19 +83,16 @@ protected B delegate() { return delegate; } -// @Override public HB tags(Iterable tags) { tagsSetter.apply(MTag.tags(tags)); return identity(); } -// @Override public HB description(String description) { descriptionSetter.apply(description); return identity(); } -// @Override public HB baseUnit(String baseUnit) { baseUnitSetter.apply(baseUnit); return identity(); @@ -109,7 +102,7 @@ public HB identity() { return (HB) this; } - abstract HM register(MeterRegistry meterRegistry); +// abstract HM register(MMeterRegistry meterRegistry); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 0dc35ea6318..32eb691bc54 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -21,7 +21,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import io.helidon.common.config.Config; +import io.helidon.metrics.api.MetricsConfig; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.DistributionSummary; @@ -38,11 +38,15 @@ class MMeterRegistry implements io.helidon.metrics.api.MeterRegistry { private static final System.Logger LOGGER = System.getLogger(MMeterRegistry.class.getName()); - static MMeterRegistry create(Config helidonConfig) { + static MMeterRegistry create(MeterRegistry meterRegistry) { + return new MMeterRegistry(meterRegistry); + } + + static MMeterRegistry create(MetricsConfig metricsConfig) { return new MMeterRegistry(new PrometheusMeterRegistry(new PrometheusConfig() { @Override public String get(String key) { - return helidonConfig.get(key).asString().orElse(null); + return metricsConfig.lookupConfig(key).orElse(null); } })); } @@ -72,16 +76,30 @@ public Collection meters(Predicate> M getOrCreate(B builder) { + public > HM getOrCreate(HB builder) { + + // The Micrometer builders do not have a shared inherited declaration of the register method. + // Each type of builder declares its own so we need to decide here which specific one to invoke. + // That's so we can invoke the Micrometer builder's register method, which acts as + // get-or-create. + Meter meter; + + // Micrometer itself will throw an IllegalArgumentException if the caller specifies a builder that finds an existing + // meter but it is of the wrong type. + if (builder instanceof MCounter.Builder cBuilder) { - return (M) cBuilder.register(delegate); + Counter counter = cBuilder.delegate().register(delegate); + meter = counter; } else if (builder instanceof MDistributionSummary.Builder sBuilder) { - return (M) sBuilder.delegate().register(delegate); + DistributionSummary summary = sBuilder.delegate().register(delegate); + meter = summary; } else if (builder instanceof MGauge.Builder gBuilder) { - return (M) gBuilder.delegate().register(delegate); + Gauge gauge = gBuilder.delegate().register(delegate); + meter = gauge; } else if (builder instanceof MTimer.Builder tBuilder) { - return (M) tBuilder.delegate().register(delegate); + Timer timer = tBuilder.delegate().register(delegate); + meter = timer; } else { throw new IllegalArgumentException(String.format("Unexpected builder type %s, expected one of %s", builder.getClass().getName(), @@ -90,6 +108,7 @@ B extends io.helidon.metrics.api.Meter.Builder> M getOrCreate(B builder) { MGauge.Builder.class.getName(), MTimer.Builder.class.getName()))); } + return (HM) meters.get(meter); } @Override @@ -148,6 +167,7 @@ public Optional remove(String name, Iterable tags) { return internalRemove(name, Util.tags(tags)); } + MeterRegistry delegate() { return delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 1f29c4cef00..31ab524f9d4 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -153,10 +153,11 @@ private Builder(String name) { super(Timer.builder(name)); } - @Override - MTimer register(MeterRegistry meterRegistry) { - return MTimer.create(delegate().register(meterRegistry)); - } + // TODO remove if not used +// @Override +// MTimer register(MeterRegistry meterRegistry) { +// return MTimer.create(delegate().register(meterRegistry)); +// } @Override public Builder publishPercentiles(double... percentiles) { diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index f5fd8a70520..0c9fb49bd34 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -17,6 +17,7 @@ import java.util.function.ToDoubleFunction; +import io.helidon.common.LazyValue; import io.helidon.metrics.api.Clock; import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.DistributionStatisticsConfig; @@ -29,6 +30,10 @@ import io.helidon.metrics.api.Tag; import io.helidon.metrics.api.Timer; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; + /** * Implementation of the neutral Helidon metrics factory based on Micrometer. */ @@ -38,13 +43,40 @@ static MicrometerMetricsFactory create(MetricsConfig metricsConfig) { return new MicrometerMetricsFactory(metricsConfig); } + private final io.micrometer.core.instrument.MeterRegistry micrometerGlobalRegistry; + + private LazyValue globalMeterRegistry; + + private final MetricsConfig metricsConfig; + private MicrometerMetricsFactory(MetricsConfig metricsConfig) { + micrometerGlobalRegistry = Metrics.globalRegistry; + this.metricsConfig = metricsConfig; + globalMeterRegistry = LazyValue.create(() -> { + ensurePrometheusRegistry(Metrics.globalRegistry, metricsConfig); + return MMeterRegistry.create(Metrics.globalRegistry); + }); + } + + @Override + public MeterRegistry createMeterRegistry(MetricsConfig metricsConfig) { + return MMeterRegistry.create(metricsConfig); } @Override public MeterRegistry globalRegistry() { - // TODO fix following null - return MMeterRegistry.create(null); + return globalMeterRegistry.get(); + } + + private static void ensurePrometheusRegistry(CompositeMeterRegistry compositeMeterRegistry, + MetricsConfig metricsConfig) { + boolean prometheusRegistryPresent = compositeMeterRegistry + .getRegistries() + .stream() + .anyMatch(mr -> mr instanceof PrometheusMeterRegistry); + if (!prometheusRegistryPresent) { + compositeMeterRegistry.add(new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(null))); + } } @Override @@ -88,7 +120,7 @@ public Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFun @Override public Timer.Builder timerBuilder(String name) { - return Timer.builder(name); + return MTimer.builder(name); } @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java index 373a2df53a6..cdd6de08558 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java @@ -15,47 +15,48 @@ */ package io.helidon.metrics.micrometer; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.spi.MetricsFactoryProvider; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; -import io.micrometer.prometheus.PrometheusConfig; -import io.micrometer.prometheus.PrometheusMeterRegistry; /** * Provides the Micrometer meter registry to use as a delegate for the implementation of the Helidon metrics API. */ public class MicrometerMetricsFactoryProvider implements MetricsFactoryProvider { - private static final String PROMETHEUS_CONFIG_CLASS_NAME = PrometheusConfig.class.getName(); - private static final String PROMETHEUS_METER_REGISTRY_CLASS_NAME = PrometheusMeterRegistry.class.getName(); - private static MeterRegistry prometheusMeterRegistry; +// private static final String PROMETHEUS_CONFIG_CLASS_NAME = PrometheusConfig.class.getName(); +// private static final String PROMETHEUS_METER_REGISTRY_CLASS_NAME = PrometheusMeterRegistry.class.getName(); +// private LazyValue meterRegistry = LazyValue.create(this::getRegistry); + +// static { +// try { +// Class prometheusConfigClass = Class.forName(PROMETHEUS_CONFIG_CLASS_NAME); +// Class prometheusMeterRegistryClass = Class.forName(PROMETHEUS_METER_REGISTRY_CLASS_NAME); +// try { +// +// Constructor ctor = prometheusMeterRegistryClass.getConstructor(PrometheusConfig.class); +// meterRegistry = (PrometheusMeterRegistry) ctor.newInstance(); +// Metrics.globalRegistry.add(meterRegistry); +// } catch (NoSuchMethodException e) { +// throw new RuntimeException("Found " + PrometheusMeterRegistry.class.getName() +// + " but unable to locate the expected constructor", e); +// } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { +// throw new RuntimeException(e); +// } +// } catch (ClassNotFoundException e) { +// meterRegistry = null; +// } +// } - static { - try { - Class prometheusConfigClass = Class.forName(PROMETHEUS_CONFIG_CLASS_NAME); - Class prometheusMeterRegistryClass = Class.forName(PROMETHEUS_METER_REGISTRY_CLASS_NAME); - try { - Constructor ctor = prometheusMeterRegistryClass.getConstructor(PrometheusConfig.class); - prometheusMeterRegistry = (PrometheusMeterRegistry) ctor.newInstance(); - Metrics.globalRegistry.add(prometheusMeterRegistry); - } catch (NoSuchMethodException e) { - throw new RuntimeException("Found " + PrometheusMeterRegistry.class.getName() - + " but unable to locate the expected constructor", e); - } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } catch (ClassNotFoundException e) { - prometheusMeterRegistry = null; - } - } @Override public MetricsFactory create(MetricsConfig metricsConfig) { return MicrometerMetricsFactory.create(metricsConfig); } + + private MeterRegistry getRegistry() { + return Metrics.globalRegistry; + } } diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java new file mode 100644 index 00000000000..1c7dd340e7a --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + + +import java.util.List; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SimpleMeterRegistryTests { + + @Test + void testConflictingMetadata() { + Counter c1 = Metrics.getOrCreate(Counter.builder("b")); + + assertThrows(IllegalArgumentException.class, () -> + Metrics.getOrCreate(Timer.builder("b"))); + } + + @Test + void testSameNameNoTags() { + Counter counter1 = Metrics.getOrCreate(Counter.builder("a")); + Counter counter2 = Metrics.getOrCreate(Counter.builder("a")); + assertThat("Counter with same name, no tags", counter1, is(sameInstance(counter2))); + } + + @Test + void testSameNameSameTwoTags() { + var tags = List.of(Tag.of("foo", "1"), + Tag.of("bar", "1")); + } + + +} From 52df7b5b3c82ff229d2123cf8d9ca2c0589ae7c2 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Thu, 10 Aug 2023 08:08:00 -0500 Subject: [PATCH 17/40] Try workaround for providing visibility to config --- .../metrics/api/MetricsConfigBlueprint.java | 18 ++++++++- .../micrometer/SimpleMeterRegistryTests.java | 40 ++++++++++++++++--- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 0f7c1c8226f..cae22b19bf2 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -19,6 +19,7 @@ import java.util.Optional; import io.helidon.builder.api.Prototype; +import io.helidon.common.config.Config; import io.helidon.common.config.GlobalConfig; import io.helidon.config.metadata.Configured; import io.helidon.config.metadata.ConfiguredOption; @@ -40,9 +41,16 @@ static Optional lookupConfig(MetricsConfig metricsConfig, String key) { return Optional.empty(); } +// @Prototype.BuilderMethod +// static MetricsConfig.BuilderBase metricsConfig(MetricsConfig.BuilderBase metricsConfigBuilder, Config config) { +// metricsConfigBuilder.config(config); +// return metricsConfigBuilder; +// } + private CustomMethods() { } } + /** * The config key containing settings for all of metrics. */ @@ -56,7 +64,7 @@ private CustomMethods() { /** * Config key for the app tag value to be applied to all metrics in this application. */ - String APP_TAG_CONFIG_KEY = "appName"; + String APP_TAG_CONFIG_KEY = "app-name"; /** * Whether metrics functionality is enabled. @@ -82,9 +90,17 @@ private CustomMethods() { @ConfiguredOption(key = GLOBAL_TAGS_CONFIG_KEY) List globalTags(); + /** + * Application tag value added to each meter ID. + * + * @return application tag value + */ @ConfiguredOption(key = APP_TAG_CONFIG_KEY) Optional appTagValue(); + @ConfiguredOption(builderMethod = false, configured = false) + Config metricsConfig(); + class BuilderDecorator implements Prototype.BuilderDecorator> { @Override diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java index 1c7dd340e7a..f2dd5391c94 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java @@ -21,10 +21,11 @@ import io.helidon.metrics.api.Counter; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; -import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.api.MetricsConfig; import io.helidon.metrics.api.Tag; import io.helidon.metrics.api.Timer; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -34,18 +35,25 @@ class SimpleMeterRegistryTests { - @Test + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test void testConflictingMetadata() { - Counter c1 = Metrics.getOrCreate(Counter.builder("b")); + Counter c1 = meterRegistry.getOrCreate(Counter.builder("b")); assertThrows(IllegalArgumentException.class, () -> - Metrics.getOrCreate(Timer.builder("b"))); + meterRegistry.getOrCreate(Timer.builder("b"))); } @Test void testSameNameNoTags() { - Counter counter1 = Metrics.getOrCreate(Counter.builder("a")); - Counter counter2 = Metrics.getOrCreate(Counter.builder("a")); + Counter counter1 = meterRegistry.getOrCreate(Counter.builder("a")); + Counter counter2 = meterRegistry.getOrCreate(Counter.builder("a")); assertThat("Counter with same name, no tags", counter1, is(sameInstance(counter2))); } @@ -53,7 +61,27 @@ void testSameNameNoTags() { void testSameNameSameTwoTags() { var tags = List.of(Tag.of("foo", "1"), Tag.of("bar", "1")); + + Counter counter1 = meterRegistry.getOrCreate(Counter.builder("c") + .tags(tags)); + Counter counter2 = meterRegistry.getOrCreate(Counter.builder("c") + .tags(tags)); + assertThat("Counter with same name, same two tags", counter1, is(sameInstance(counter2))); } + @Test + void testSameNameOverlappingButDifferentTags() { + var tags1 = List.of(Tag.of("foo", "1"), + Tag.of("bar", "1"), + Tag.of("baz", "1")); + + var tags2 = List.of(Tag.of("foo", "1"), + Tag.of("bar", "1")); + meterRegistry.getOrCreate(Counter.builder("c") + .tags(tags1)); + assertThrows(IllegalArgumentException.class, () -> + meterRegistry.getOrCreate(Counter.builder("c") + .tags(tags2))); + } } From dada8d2c986ceab07554f00be7ba787a9a22e4c8 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 11 Aug 2023 08:40:00 -0500 Subject: [PATCH 18/40] Resolve merge conflict re: Fix from Tomas for builder --- .../java/io/helidon/builder/processor/PrototypeProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java b/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java index e7860a986f0..07283b7b318 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java @@ -320,7 +320,7 @@ private static ConfiguredOption configuredFromAnnotation(TypeHandler typeHandler .map(typeHandler::toDefaultValue) .orElse(null), configuredAnnotation.getValue("builderMethod").map(Boolean::parseBoolean).orElse(true), - configuredAnnotation.getValue("notConfigured").map(Boolean::parseBoolean).orElse(false), + !configuredAnnotation.getValue("configured").map(Boolean::parseBoolean).orElse(true), provider, provider ? ProviderOption.create(configuredAnnotation) : null); } From 2c6f202bd8b3a2a208e9d43283d7f1a820550ee9 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 11 Aug 2023 08:41:39 -0500 Subject: [PATCH 19/40] Adding more tests in the Micrometer implementation based on the old unit tests; some changes triggered by those tests --- .../java/io/helidon/metrics/api/Meter.java | 2 +- .../io/helidon/metrics/api/MeterRegistry.java | 8 +-- .../java/io/helidon/metrics/api/Metrics.java | 8 +-- .../metrics/api/MetricsConfigBlueprint.java | 58 ++++++++++++------- .../helidon/metrics/api/MetricsFactory.java | 2 +- .../io/helidon/metrics/api/NoOpMeter.java | 2 +- .../metrics/api/NoOpMetricsFactory.java | 2 +- .../java/io/helidon/metrics/api/NoOpTag.java | 4 +- .../main/java/io/helidon/metrics/api/Tag.java | 4 +- .../io/helidon/metrics/micrometer/MMeter.java | 37 +++++++++++- .../metrics/micrometer/MMeterRegistry.java | 49 +++++++--------- .../io/helidon/metrics/micrometer/MTag.java | 26 +++++++++ .../micrometer/MicrometerMetricsFactory.java | 5 +- .../io/helidon/metrics/micrometer/Util.java | 2 +- .../micrometer/SimpleMeterRegistryTests.java | 20 +------ 15 files changed, 142 insertions(+), 87 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index 422211ba0c4..547a451bfdc 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -78,7 +78,7 @@ interface Id { * * @return meter tags */ - Iterable tags(); + Iterable tags(); /** * Unwraps the ID as the specified type. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 66b2602554e..749697c2a3a 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -58,7 +58,7 @@ public interface MeterRegistry extends Wrapped { * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered counter; empty if not found */ - default Optional getCounter(String name, Iterable tags) { + default Optional getCounter(String name, Iterable tags) { return get(Counter.class, name, tags); } @@ -69,7 +69,7 @@ default Optional getCounter(String name, Iterable tags) * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered distribution summary; empty if not found */ - default Optional getSummary(String name, Iterable tags) { + default Optional getSummary(String name, Iterable tags) { return get(DistributionSummary.class, name, tags); } @@ -80,7 +80,7 @@ default Optional getSummary(String name, Iterable * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered gauge; empty if not found */ - default Optional getGauge(String name, Iterable tags) { + default Optional getGauge(String name, Iterable tags) { return get(Gauge.class, name, tags); } @@ -91,7 +91,7 @@ default Optional getGauge(String name, Iterable tags) { * @param tags tags to match * @return {@link java.util.Optional} of the previously-registered timer; empty if not found */ - default Optional getTimer(String name, Iterable tags) { + default Optional getTimer(String name, Iterable tags) { return get(Timer.class, name, tags); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 3fe86c2a593..55fac159a31 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -168,7 +168,7 @@ static Optional get(Class mClass, String name, Iterable< * @return new tag */ static Tag tag(String key, String value) { - return MetricsFactory.getInstance().tagOf(key, value); + return MetricsFactory.getInstance().tagCreate(key, value); } /** @@ -193,8 +193,8 @@ public Tag next() { throw new NoSuchElementException(); } Tag result = MetricsFactoryManager.getInstance() - .tagOf(tags[slot].key(), - tags[slot].value()); + .tagCreate(tags[slot].key(), + tags[slot].value()); slot++; return result; } @@ -226,7 +226,7 @@ public Tag next() { if (!hasNext()) { throw new NoSuchElementException(); } - Tag result = Tag.of(keyValuePairs[slot * 2], keyValuePairs[slot * 2 + 1]); + Tag result = Tag.create(keyValuePairs[slot * 2], keyValuePairs[slot * 2 + 1]); slot++; return result; } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index cae22b19bf2..29fd58f8868 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -15,8 +15,11 @@ */ package io.helidon.metrics.api; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.TreeMap; import io.helidon.builder.api.Prototype; import io.helidon.common.config.Config; @@ -31,26 +34,9 @@ @ConfigBean() @Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY) @Prototype.Blueprint(decorator = MetricsConfigBlueprint.BuilderDecorator.class) -@Prototype.CustomMethods(MetricsConfigBlueprint.CustomMethods.class) +@Prototype.CustomMethods(MetricsConfigSupport.class) interface MetricsConfigBlueprint { - class CustomMethods { - - @Prototype.PrototypeMethod - static Optional lookupConfig(MetricsConfig metricsConfig, String key) { - return Optional.empty(); - } - -// @Prototype.BuilderMethod -// static MetricsConfig.BuilderBase metricsConfig(MetricsConfig.BuilderBase metricsConfigBuilder, Config config) { -// metricsConfigBuilder.config(config); -// return metricsConfigBuilder; -// } - - private CustomMethods() { - } - } - /** * The config key containing settings for all of metrics. */ @@ -104,10 +90,40 @@ private CustomMethods() { class BuilderDecorator implements Prototype.BuilderDecorator> { @Override - public void decorate(MetricsConfig.BuilderBase target) { - if (target.config().isEmpty()) { - target.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); + public void decorate(MetricsConfig.BuilderBase builder) { + if (builder.config().isEmpty()) { + builder.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); + } + builder.metricsConfig(builder.config().get()); + } + } + + @Prototype.FactoryMethod + static List createGlobalTags(Config globalTagExpression) { + String pairs = globalTagExpression.asString().get(); + // Use a TreeMap to order by tag name. + Map result = new TreeMap<>(); + List errorPairs = new ArrayList<>(); + String[] assignments = pairs.split(","); + for (String assignment : assignments) { + int equalsSlot = assignment.indexOf("="); + if (equalsSlot == -1) { + errorPairs.add("Missing '=': " + assignment); + } else if (equalsSlot == 0) { + errorPairs.add("Missing tag name: " + assignment); + } else if (equalsSlot == assignment.length() - 1) { + errorPairs.add("Missing tag value: " + assignment); + } else { + String key = assignment.substring(0, equalsSlot); + result.put(key, + Tag.create(key, assignment.substring(equalsSlot + 1))); } } + if (!errorPairs.isEmpty()) { + throw new IllegalArgumentException("Error(s) in global tag expression: " + errorPairs); + } + return result.values() + .stream() + .toList(); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index faca7a9cda6..8596ebbd8e3 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -151,7 +151,7 @@ static MetricsFactory getInstance() { * @param value tag value * @return new {@code Tag} instance */ - Tag tagOf(String key, String value); + Tag tagCreate(String key, String value); /** * Returns an empty histogram snapshot with the specified aggregate values. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index f2214920cd8..0862d873e15 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -154,7 +154,7 @@ public B tags(Iterable tags) { } public B tag(String key, String value) { - tags.put(key, Tag.of(key, value)); + tags.put(key, Tag.create(key, value)); return identity(); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 94897421042..1301733c36b 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -56,7 +56,7 @@ public Clock clockSystem() { } @Override - public Tag tagOf(String key, String value) { + public Tag tagCreate(String key, String value) { return new NoOpTag(key, value); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java index 933124f1f9c..23e13c90bbf 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java @@ -20,7 +20,7 @@ record NoOpTag(String key, String value) implements Tag { - static Tag of(String key, String value) { + static Tag create(String key, String value) { return new NoOpTag(key, value); } @@ -42,7 +42,7 @@ public Tag next() { if (!hasNext()) { throw new NoSuchElementException(); } - Tag result = Tag.of(keysAndValues[2 * slot], keysAndValues[2 * slot + 1]); + Tag result = Tag.create(keysAndValues[2 * slot], keysAndValues[2 * slot + 1]); slot++; return result; } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java index a2b9b5857da..96bc14f5ad7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java @@ -41,7 +41,7 @@ public interface Tag extends Wrapped { * @param value the tag's value * @return new {@code Tag} representing the key and value */ - static Tag of(String key, String value) { - return MetricsFactory.getInstance().tagOf(key, value); + static Tag create(String key, String value) { + return MetricsFactory.getInstance().tagCreate(key, value); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index a4c5dcab543..07573aafb86 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -16,6 +16,7 @@ package io.helidon.metrics.micrometer; import java.util.Iterator; +import java.util.Objects; import java.util.function.Function; import io.micrometer.core.instrument.Meter; @@ -56,6 +57,23 @@ public Type type() { .name()); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MMeter mMeter = (MMeter) o; + return Objects.equals(delegate, mMeter.delegate); + } + + @Override + public int hashCode() { + return Objects.hash(delegate); + } + protected M delegate() { return delegate; } @@ -124,7 +142,7 @@ public String name() { } @Override - public Iterable tags() { + public Iterable tags() { return new Iterable<>() { private final Iterator iter = delegate.getTags().iterator(); @@ -144,5 +162,22 @@ public io.helidon.metrics.api.Tag next() { } }; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Id id = (Id) o; + return Objects.equals(delegate, id.delegate); + } + + @Override + public int hashCode() { + return Objects.hash(delegate); + } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 32eb691bc54..fb71f600156 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -30,36 +30,40 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.search.Search; -import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; class MMeterRegistry implements io.helidon.metrics.api.MeterRegistry { private static final System.Logger LOGGER = System.getLogger(MMeterRegistry.class.getName()); - static MMeterRegistry create(MeterRegistry meterRegistry) { - return new MMeterRegistry(meterRegistry); + static MMeterRegistry create(MeterRegistry meterRegistry, + MetricsConfig metricsConfig) { + return new MMeterRegistry(meterRegistry, metricsConfig); } static MMeterRegistry create(MetricsConfig metricsConfig) { - return new MMeterRegistry(new PrometheusMeterRegistry(new PrometheusConfig() { - @Override - public String get(String key) { - return metricsConfig.lookupConfig(key).orElse(null); - } - })); + CompositeMeterRegistry delegate = new CompositeMeterRegistry(); + delegate.add(new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(null))); + return new MMeterRegistry(delegate, metricsConfig); } private final MeterRegistry delegate; private final ConcurrentHashMap meters = new ConcurrentHashMap<>(); - private MMeterRegistry(MeterRegistry delegate) { + private MMeterRegistry(MeterRegistry delegate, + MetricsConfig metricsConfig) { this.delegate = delegate; delegate.config() .onMeterAdded(this::recordAdd) .onMeterRemoved(this::recordRemove); + List globalTags = metricsConfig.globalTags(); + if (!globalTags.isEmpty()) { + delegate.config().meterFilter(MeterFilter.commonTags(Util.tags(globalTags))); + } } @Override @@ -79,15 +83,16 @@ public Collection meters(Predicate> HM getOrCreate(HB builder) { + // The Micrometer builders do not have a shared inherited declaration of the register method. // Each type of builder declares its own so we need to decide here which specific one to invoke. // That's so we can invoke the Micrometer builder's register method, which acts as // get-or-create. - Meter meter; - - // Micrometer itself will throw an IllegalArgumentException if the caller specifies a builder that finds an existing - // meter but it is of the wrong type. + // Micrometer's register methods will throw an IllegalArgumentException if the caller specifies a builder that finds + // a previously-registered meter of a different type from that implied by the builder. + Meter meter; + // TODO Convert to switch instanceof expressions once checkstyle understand the syntax. if (builder instanceof MCounter.Builder cBuilder) { Counter counter = cBuilder.delegate().register(delegate); meter = counter; @@ -118,20 +123,8 @@ public Optional get(Class mClass, Search search = delegate().find(name) .tags(Util.tags(tags)); - Meter match; - - if (io.helidon.metrics.api.Counter.class.isAssignableFrom(mClass)) { - match = search.counter(); - } else if (io.helidon.metrics.api.DistributionSummary.class.isAssignableFrom(mClass)) { - match = search.summary(); - } else if (io.helidon.metrics.api.Gauge.class.isAssignableFrom(mClass)) { - match = search.gauge(); - } else if (io.helidon.metrics.api.Timer.class.isAssignableFrom(mClass)) { - match = search.timer(); - } else { - throw new IllegalArgumentException( - String.format("Provided class %s is not recognized", mClass.getName())); - } + Meter match = search.meter(); + if (match == null) { return Optional.empty(); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java index 6239d88998a..3e9cb1b814d 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java @@ -16,6 +16,8 @@ package io.helidon.metrics.micrometer; import java.util.Iterator; +import java.util.Objects; +import java.util.StringJoiner; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; @@ -96,4 +98,28 @@ public String key() { public String value() { return delegate.getValue(); } + + @Override + public String toString() { + return new StringJoiner(", ", MTag.class.getSimpleName() + "[", "]") + .add(key() + "=" + value()) + .toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MTag mTag = (MTag) o; + return Objects.equals(delegate, mTag.delegate); + } + + @Override + public int hashCode() { + return Objects.hash(delegate.hashCode()); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index 0c9fb49bd34..dab706e0eaf 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -52,9 +52,10 @@ static MicrometerMetricsFactory create(MetricsConfig metricsConfig) { private MicrometerMetricsFactory(MetricsConfig metricsConfig) { micrometerGlobalRegistry = Metrics.globalRegistry; this.metricsConfig = metricsConfig; + globalMeterRegistry = LazyValue.create(() -> { ensurePrometheusRegistry(Metrics.globalRegistry, metricsConfig); - return MMeterRegistry.create(Metrics.globalRegistry); + return MMeterRegistry.create(Metrics.globalRegistry, metricsConfig); }); } @@ -139,7 +140,7 @@ public Timer.Sample timerStart(Clock clock) { } @Override - public Tag tagOf(String key, String value) { + public Tag tagCreate(String key, String value) { return MTag.of(key, value); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java index 96175c31d37..b0f7773bbba 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java @@ -83,7 +83,7 @@ public boolean hasNext() { @Override public io.helidon.metrics.api.Tag next() { Tag next = tagsIter.next(); - return io.helidon.metrics.api.Tag.of(next.getKey(), next.getValue()); + return io.helidon.metrics.api.Tag.create(next.getKey(), next.getValue()); } }; } diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java index f2dd5391c94..87eac3e92df 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java @@ -59,8 +59,8 @@ void testSameNameNoTags() { @Test void testSameNameSameTwoTags() { - var tags = List.of(Tag.of("foo", "1"), - Tag.of("bar", "1")); + var tags = List.of(Tag.create("foo", "1"), + Tag.create("bar", "1")); Counter counter1 = meterRegistry.getOrCreate(Counter.builder("c") .tags(tags)); @@ -68,20 +68,4 @@ void testSameNameSameTwoTags() { .tags(tags)); assertThat("Counter with same name, same two tags", counter1, is(sameInstance(counter2))); } - - @Test - void testSameNameOverlappingButDifferentTags() { - var tags1 = List.of(Tag.of("foo", "1"), - Tag.of("bar", "1"), - Tag.of("baz", "1")); - - var tags2 = List.of(Tag.of("foo", "1"), - Tag.of("bar", "1")); - - meterRegistry.getOrCreate(Counter.builder("c") - .tags(tags1)); - assertThrows(IllegalArgumentException.class, () -> - meterRegistry.getOrCreate(Counter.builder("c") - .tags(tags2))); - } } From e516cd706503e6870d39e988fbaac0d6befcb3ef Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 11 Aug 2023 11:15:25 -0500 Subject: [PATCH 20/40] Add straggler new file --- .../metrics/api/MetricsConfigSupport.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java new file mode 100644 index 00000000000..59e9bcf6b55 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.Optional; + +import io.helidon.builder.api.Prototype; + +class MetricsConfigSupport { + + @Prototype.PrototypeMethod + static Optional lookupConfig(MetricsConfig metricsConfig, String key) { + return metricsConfig.metricsConfig() + .get(key) + .asString() + .asOptional(); + } + + private MetricsConfigSupport() { + } +} From e1eb00504b6f6de86d4319756cf6ec8eb2347635 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 11 Aug 2023 11:30:30 -0500 Subject: [PATCH 21/40] Add a missing requires clause --- metrics/providers/micrometer/src/main/java/module-info.java | 1 + 1 file changed, 1 insertion(+) diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index cca63fcb3d4..8232d0e9cd2 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -24,6 +24,7 @@ requires io.helidon.metrics.api; requires micrometer.core; requires static micrometer.registry.prometheus; + requires io.helidon.common; requires io.helidon.common.config; provides io.helidon.metrics.spi.MetricsFactoryProvider with MicrometerMetricsFactoryProvider; From a9bbe28a2cf9af0c9733587e1aa786baabbb5f33 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Fri, 11 Aug 2023 16:01:37 -0500 Subject: [PATCH 22/40] More tests and associated code changes --- .../metrics/api/MetricsConfigBlueprint.java | 59 +++++++++---- .../metrics/api/MetricsConfigSupport.java | 7 ++ .../api/TestMetricsConfigTagsHandling.java | 83 +++++++++++++++++++ 3 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 metrics/api/src/test/java/io/helidon/metrics/api/TestMetricsConfigTagsHandling.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 29fd58f8868..267e0046cc7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Optional; import java.util.TreeMap; +import java.util.regex.Matcher; import io.helidon.builder.api.Prototype; import io.helidon.common.config.Config; @@ -100,27 +101,55 @@ public void decorate(MetricsConfig.BuilderBase builder) { @Prototype.FactoryMethod static List createGlobalTags(Config globalTagExpression) { - String pairs = globalTagExpression.asString().get(); + return createGlobalTags(globalTagExpression.asString().get()); + } + + static List createGlobalTags(String pairs) { // Use a TreeMap to order by tag name. Map result = new TreeMap<>(); - List errorPairs = new ArrayList<>(); - String[] assignments = pairs.split(","); + List allErrors = new ArrayList<>(); + String[] assignments = pairs.split("(? errorsForThisAssignment = new ArrayList<>(); + if (assignment.isBlank()) { + errorsForThisAssignment.add("empty assignment at position " + position + ": " + assignment); } else { - String key = assignment.substring(0, equalsSlot); - result.put(key, - Tag.create(key, assignment.substring(equalsSlot + 1))); + // Pattern should yield group 1 = tag name and group 2 = tag value. + Matcher matcher = MetricsConfigSupport.TAG_ASSIGNMENT_PATTERN.matcher(assignment); + if (!matcher.matches()) { + errorsForThisAssignment.add("expected tag=value but found '" + assignment + "'"); + } else { + String name = matcher.group(1); + String value = matcher.group(2); + if (name.isBlank()) { + errorsForThisAssignment.add("missing tag name"); + } + if (value.isBlank()) { + errorsForThisAssignment.add("missing tag value"); + } + if (!name.matches("[A-Za-z_][A-Za-z_0-9]*")) { + errorsForThisAssignment.add( + "tag name must start with a letter and include only letters, digits, and underscores"); + } + if (errorsForThisAssignment.isEmpty()) { + result.put(name, + Tag.create(name, + value.replace("\\,", ",") + .replace("\\=", "="))); + } + } + } + if (!errorsForThisAssignment.isEmpty()) { + allErrors.add(String.format("Position %d with expression %s: %s", + position, + assignment, + errorsForThisAssignment)); } + position++; } - if (!errorPairs.isEmpty()) { - throw new IllegalArgumentException("Error(s) in global tag expression: " + errorPairs); + if (!allErrors.isEmpty()) { + throw new IllegalArgumentException("Error(s) in global tag expression: " + allErrors); } return result.values() .stream() diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java index 59e9bcf6b55..b481b3d5769 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java @@ -16,6 +16,7 @@ package io.helidon.metrics.api; import java.util.Optional; +import java.util.regex.Pattern; import io.helidon.builder.api.Prototype; @@ -29,6 +30,12 @@ static Optional lookupConfig(MetricsConfig metricsConfig, String key) { .asOptional(); } + // Pattern: + // - capture reluctant match of anything + // - non-capturing match of an unescaped = + // - capture the rest. + static final Pattern TAG_ASSIGNMENT_PATTERN = Pattern.compile("(.*?)(?:(? + MetricsConfigBlueprint.createGlobalTags(",a=1")); + assertThat("Empty assignment", ex.getMessage(), containsString("empty")); + } + + @Test + void checkNoRightSide() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> + MetricsConfigBlueprint.createGlobalTags("a=")); + assertThat("No right side", ex.getMessage(), containsString("missing tag value")); + } + + @Test + void checkNoLeftSide() { + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> + MetricsConfigBlueprint.createGlobalTags("=1")); + assertThat("No left side", ex.getMessage(), containsString("missing tag name")); + } + + @Test + void checkInvalidTagName() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> + MetricsConfigBlueprint.createGlobalTags("a*=1,")); + assertThat("Invalid tag name", ex.getMessage(), containsString("tag name must")); + } +} From 5ea43b88d527bf77b8afd99433f5d0acdbead1d1 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sat, 12 Aug 2023 22:20:09 -0500 Subject: [PATCH 23/40] More tests; push for safe keeping --- .../metrics/api/DistributionSummary.java | 7 + .../io/helidon/metrics/api/NoOpMeter.java | 13 +- .../metrics/api/NoOpMeterRegistry.java | 2 +- .../metrics/api/NoOpMetricsFactory.java | 5 + .../java/io/helidon/metrics/api/NoOpTag.java | 2 +- .../io/helidon/metrics/api/NoOpWrapped.java | 24 ++++ .../java/io/helidon/metrics/api/Wrapped.java | 9 +- .../io/helidon/metrics/micrometer/MClock.java | 5 + .../metrics/micrometer/MCountAtBucket.java | 5 + .../MDistributionStatisticsConfig.java | 14 ++ .../micrometer/MDistributionSummary.java | 29 ++++- .../micrometer/MHistogramSnapshot.java | 7 +- .../io/helidon/metrics/micrometer/MMeter.java | 5 + .../metrics/micrometer/MMeterRegistry.java | 5 + .../io/helidon/metrics/micrometer/MTag.java | 5 + .../micrometer/MValueAtPercentile.java | 7 +- .../micrometer/MicrometerMetricsFactory.java | 5 + .../metrics/micrometer/TestCounter.java | 78 +++++++++++ .../metrics/micrometer/TestDeletions.java | 121 ++++++++++++++++++ .../micrometer/TestDistributionSummary.java | 94 ++++++++++++++ .../metrics/micrometer/TestGlobalTags.java | 76 +++++++++++ 21 files changed, 499 insertions(+), 19 deletions(-) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java index 7ae423ea595..35c3898ab57 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -69,6 +69,13 @@ static Builder builder(String name, */ double max(); + /** + * Returns a {@link io.helidon.metrics.api.HistogramSnapshot} of the current state of the distribution summary. + * + * @return snapshot + */ + HistogramSnapshot snapshot(); + /** * Builder for a {@link io.helidon.metrics.api.DistributionSummary}. */ diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 0862d873e15..404dbc7f442 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -34,7 +34,7 @@ /** * No-op implementation of the Helidon {@link io.helidon.metrics.api.Meter} interface. */ -class NoOpMeter implements Meter { +class NoOpMeter implements Meter, NoOpWrapped { private final Id id; private final String unit; @@ -323,9 +323,14 @@ public double mean() { public double max() { return 0; } + + @Override + public io.helidon.metrics.api.HistogramSnapshot snapshot() { + return new NoOpMeter.HistogramSnapshot(0L, 0D, 0D); + } } - static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot { + static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot, NoOpWrapped { private final long count; private final double total; @@ -575,13 +580,13 @@ public double max(TimeUnit unit) { } } - static class DistributionStatisticsConfig implements io.helidon.metrics.api.DistributionStatisticsConfig { + static class DistributionStatisticsConfig implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapped { static Builder builder() { return new Builder(); } - static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder { + static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder, NoOpWrapped { @Override public io.helidon.metrics.api.DistributionStatisticsConfig build() { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 6d43bdd28be..6b235e8c8a0 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -31,7 +31,7 @@ * store meters or their IDs, in line with the documented behavior of disabled metrics. *

*/ -class NoOpMeterRegistry implements MeterRegistry { +class NoOpMeterRegistry implements MeterRegistry, NoOpWrapped { private final Map meters = new ConcurrentHashMap<>(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 1301733c36b..655deb4df74 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -25,6 +25,11 @@ class NoOpMetricsFactory implements MetricsFactory { private final MeterRegistry meterRegistry = new NoOpMeterRegistry(); private static final Clock SYSTEM_CLOCK = new Clock() { + @Override + public R unwrap(Class c) { + return c.cast(this); + } + @Override public long wallTime() { return System.currentTimeMillis(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java index 23e13c90bbf..8d3e3364de7 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java @@ -18,7 +18,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; -record NoOpTag(String key, String value) implements Tag { +record NoOpTag(String key, String value) implements Tag, NoOpWrapped { static Tag create(String key, String value) { return new NoOpTag(key, value); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java new file mode 100644 index 00000000000..6ac9c49ae60 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +interface NoOpWrapped extends Wrapped { + + @Override + default R unwrap(Class c) { + return c.cast(this); + } +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java index e1a7cac7651..5f4ed610f8d 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java @@ -27,12 +27,5 @@ public interface Wrapped { * @return this object cast as the requested type * @param type to cast to */ - default R unwrap(Class c) { - if (c.isInstance(this)) { - return c.cast(this); - } - throw new IllegalArgumentException(String.format("Cannot provide an object of %s from an object of type %s", - c.getName(), - getClass().getName())); - } + R unwrap(Class c); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java index 98b1749befa..d49c621ee73 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java @@ -42,4 +42,9 @@ public long monotonicTime() { io.micrometer.core.instrument.Clock delegate() { return delegate; } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java index 70a8cfe91e6..2ac09ac148f 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java @@ -45,4 +45,9 @@ public double bucket(TimeUnit unit) { public double count() { return delegate.count(); } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java index 57e2670099f..6e29ce36fba 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java @@ -131,6 +131,15 @@ public Optional> serviceLevelObjectiveBoundaries() { return Optional.ofNullable(Util.iterable(delegate.getServiceLevelObjectiveBoundaries())); } + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } + + io.micrometer.core.instrument.distribution.DistributionStatisticConfig delegate() { + return delegate; + } + static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder { private final DistributionStatisticConfig.Builder delegate; @@ -204,6 +213,11 @@ public Builder serviceLevelObjectives(Iterable slos) { return this; } + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } + DistributionStatisticConfig.Builder delegate() { return delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index db59064416f..d486294c9e2 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -15,12 +15,17 @@ */ package io.helidon.metrics.micrometer; +import io.helidon.metrics.api.DistributionStatisticsConfig; +import io.helidon.metrics.api.HistogramSnapshot; + import io.micrometer.core.instrument.DistributionSummary; +import static io.micrometer.core.instrument.distribution.DistributionStatisticConfig.DEFAULT; + class MDistributionSummary extends MMeter implements io.helidon.metrics.api.DistributionSummary { static Builder builder(String name, - io.helidon.metrics.api.DistributionStatisticsConfig.Builder configBuilder) { + DistributionStatisticsConfig.Builder configBuilder) { return new Builder(name, configBuilder); } @@ -57,11 +62,29 @@ public double max() { return delegate().max(); } + @Override + public HistogramSnapshot snapshot() { + return MHistogramSnapshot.create(delegate().takeSnapshot()); + } + static class Builder extends MMeter.Builder implements io.helidon.metrics.api.DistributionSummary.Builder { - private Builder(String name, io.helidon.metrics.api.DistributionStatisticsConfig.Builder configBuilder) { - super(DistributionSummary.builder(name)); + private Builder(String name, DistributionStatisticsConfig.Builder configBuilder) { + this(name, configBuilder.build()); + } + private Builder(String name, DistributionStatisticsConfig config) { + super(DistributionSummary.builder(name) + .percentilePrecision(config.percentilePrecision() + .orElse(DEFAULT.getPercentilePrecision())) + .minimumExpectedValue(config.minimumExpectedValue() + .orElse(DEFAULT.getMinimumExpectedValueAsDouble())) + .maximumExpectedValue(config.maximumExpectedValue() + .orElse(DEFAULT.getMaximumExpectedValueAsDouble())) + .distributionStatisticExpiry(config.expiry() + .orElse(DEFAULT.getExpiry())) + .distributionStatisticBufferLength(config.bufferLength() + .orElse(DEFAULT.getBufferLength()))); } @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java index 3f6882d2314..019c7f95402 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java @@ -83,7 +83,7 @@ public io.helidon.metrics.api.ValueAtPercentile next() { if (!hasNext()) { throw new NoSuchElementException(); } - return MValueAtPercentile.of(values[slot++]); + return MValueAtPercentile.create(values[slot++]); } }; } @@ -115,4 +115,9 @@ public void outputSummary(PrintStream out, double scale) { delegate.outputSummary(out, scale); } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java index 07573aafb86..54753baa225 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java @@ -74,6 +74,11 @@ public int hashCode() { return Objects.hash(delegate); } + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } + protected M delegate() { return delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index fb71f600156..6f7cfff8e0c 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -161,6 +161,11 @@ public Optional remove(String name, return internalRemove(name, Util.tags(tags)); } + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } + MeterRegistry delegate() { return delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java index 3e9cb1b814d..f4492a721f9 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java @@ -122,4 +122,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(delegate.hashCode()); } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java index 37bb2b8e077..ae9fc77796c 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java @@ -21,7 +21,7 @@ class MValueAtPercentile implements io.helidon.metrics.api.ValueAtPercentile { - static MValueAtPercentile of(ValueAtPercentile delegate) { + static MValueAtPercentile create(ValueAtPercentile delegate) { return new MValueAtPercentile(delegate); } @@ -45,4 +45,9 @@ public double value() { public double value(TimeUnit unit) { return delegate.value(unit); } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index dab706e0eaf..d7d46eb87b9 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -95,6 +95,11 @@ public long wallTime() { public long monotonicTime() { return delegate.monotonicTime(); } + + @Override + public R unwrap(Class c) { + return c.cast(delegate); + } }; } diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java new file mode 100644 index 00000000000..319faa3b754 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +class TestCounter { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testIncr() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(); + assertThat("After increment", c.count(), is(1D)); + }; + + @Test + void incrWithValue() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(3D); + assertThat("After increment", c.count(), is(3D)); + } + + @Test + void incrBoth() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c3")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(2D); + assertThat("After increment", c.count(), is(2D)); + + Counter cAgain = meterRegistry.getOrCreate(Counter.builder("c3")); + assertThat("Looked up instance", cAgain, is(sameInstance(c))); + assertThat("Value after one update", cAgain.count(), is(2D)); + + cAgain.increment(3D); + assertThat("Value after second update", cAgain.count(), is(5D)); + } + + @Test + void testUnwrap() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c4")); + io.micrometer.core.instrument.Counter mCounter = c.unwrap(io.micrometer.core.instrument.Counter.class); + assertThat("Initial value", c.count(), is(0D)); + mCounter.increment(); + assertThat("Updated value", c.count(), is(1D)); + } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java new file mode 100644 index 00000000000..377fd5b4d02 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + + +import java.util.List; +import java.util.Optional; + +import io.helidon.common.testing.junit5.OptionalMatcher; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Tag; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + + +class TestDeletions { + + private static final String COMMON_COUNTER_NAME = "theCounter"; + + private static MeterRegistry reg; + + @BeforeAll + static void setup() { + reg = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @BeforeEach + void clear() { + reg.meters().forEach(reg::remove); + } + + @Test + void testDeleteByNameAndTags() { + Counter counter1 = reg.getOrCreate(Counter.builder("a") + .tags(List.of(Tag.create("myTag", "a")))); + Counter counter2 = reg.getOrCreate(Counter.builder("b") + .tags(List.of(Tag.create("myTag", "b")))); + + Optional counter1Again = reg.getCounter("a", List.of(Tag.create("myTag", "a"))); + assertThat("Counter before removal", counter1Again, OptionalMatcher.optionalValue(is(sameInstance(counter1)))); + Optional del1 = reg.remove("a", List.of(Tag.create("myTag", "a"))); + + assertThat("Deleted meter", del1, OptionalMatcher.optionalValue(sameInstance(counter1))); + assertThat("Check for deleted meter", + reg.getCounter("a", + List.of(Tag.create("myTag", "a"))), + OptionalMatcher.optionalEmpty()); + + assertThat("Check for undeleted meter", + reg.getCounter("b", + List.of(Tag.create("myTag", "b"))), + OptionalMatcher.optionalValue(sameInstance(counter2))); + } + @Test + void testDeleteByMeter() { + Counter counter1 = reg.getOrCreate(Counter.builder("a") + .tags(List.of(Tag.create("myTag", "a")))); + Counter counter2 = reg.getOrCreate(Counter.builder("b") + .tags(List.of(Tag.create("myTag", "b")))); + + Optional counter1Again = reg.getCounter("a", List.of(Tag.create("myTag", "a"))); + assertThat("Counter before removal", counter1Again, OptionalMatcher.optionalValue(is(sameInstance(counter1)))); + Optional del1 = reg.remove(counter1); + + assertThat("Deleted meter", del1, OptionalMatcher.optionalValue(sameInstance(counter1))); + assertThat("Check for deleted meter", + reg.getCounter("a", + List.of(Tag.create("myTag", "a"))), + OptionalMatcher.optionalEmpty()); + + assertThat("Check for undeleted meter", + reg.getCounter("b", + List.of(Tag.create("myTag", "b"))), + OptionalMatcher.optionalValue(sameInstance(counter2))); + } + @Test + void testDeleteById() { + Counter counter1 = reg.getOrCreate(Counter.builder("a") + .tags(List.of(Tag.create("myTag", "a")))); + Counter counter2 = reg.getOrCreate(Counter.builder("b") + .tags(List.of(Tag.create("myTag", "b")))); + + Optional counter1Again = reg.getCounter("a", List.of(Tag.create("myTag", "a"))); + assertThat("Counter before removal", counter1Again, OptionalMatcher.optionalValue(is(sameInstance(counter1)))); + Optional del1 = reg.remove(counter1.id()); + + assertThat("Deleted meter", del1, OptionalMatcher.optionalValue(sameInstance(counter1))); + assertThat("Check for deleted meter", + reg.getCounter("a", + List.of(Tag.create("myTag", "a"))), + OptionalMatcher.optionalEmpty()); + + assertThat("Check for undeleted meter", + reg.getCounter("b", + List.of(Tag.create("myTag", "b"))), + OptionalMatcher.optionalValue(sameInstance(counter2))); + } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java new file mode 100644 index 00000000000..06915b31d6e --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.helidon.metrics.api.CountAtBucket; +import io.helidon.metrics.api.DistributionStatisticsConfig; +import io.helidon.metrics.api.DistributionSummary; +import io.helidon.metrics.api.HistogramSnapshot; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.ValueAtPercentile; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class TestDistributionSummary { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + private static DistributionSummary commonPrep(String name) { + DistributionStatisticsConfig.Builder statsConfigBuilder = DistributionStatisticsConfig.builder() + .percentilesHistogram(true) + .percentiles(0.5, 0.9, 0.99, 0.999); + DistributionSummary summary = meterRegistry.getOrCreate(DistributionSummary.builder(name, statsConfigBuilder)); + List.of(1D, 3D, 5D, 7D) + .forEach(summary::record); + return summary; + } + + @Test + void testBasicStats() { + DistributionSummary summary = commonPrep("a"); + assertThat("Mean", summary.mean(), is(4D)); + assertThat("Min", summary.max(), is(7D)); + assertThat("Count", summary.count(), is(4L)); + assertThat("Total", summary.totalAmount(), is(16D)); + } + + @Test + void testUnwrap() { + DistributionSummary summary = commonPrep("b"); + io.micrometer.core.instrument.DistributionSummary s = summary.unwrap(io.micrometer.core.instrument.DistributionSummary.class); + assertThat("Initial count", s.count(), is(4L)); + s.record(9D); + assertThat("Count after native update", summary.count(), is(5L)); + assertThat("Total after native update", summary.totalAmount(), is(25D)); + + io.micrometer.core.instrument.distribution.HistogramSnapshot h = s.takeSnapshot(); + assertThat("Native snapshot mean", h.mean(), is(5D)); + } + + @Test + void testSnapshot() { + DistributionSummary summary = commonPrep("c"); + HistogramSnapshot snapshot = summary.snapshot(); + assertThat("Snapshot count", snapshot.count(), is(4L)); + assertThat("Snapshot total", snapshot.total(), is(16D)); + assertThat("Snapshot total as time (microseconds)", snapshot.total(TimeUnit.MICROSECONDS), is(0.016)); + } + + @Test + void testPercentiles() { + DistributionSummary summary = commonPrep("d"); + HistogramSnapshot snapshot = summary.snapshot(); + + List vaps = Util.list(snapshot.percentileValues()); + List cabs = Util.list(snapshot.histogramCounts()); + } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java new file mode 100644 index 00000000000..26abdd8d1b8 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.List; +import java.util.Map; + +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Tag; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; + +class TestGlobalTags { + + @Test + void testWithoutConfig() { + List globalTags = List.of(Tag.create("g1", "v1"), + Tag.create("g2", "v2")); + MeterRegistry meterRegistry = Metrics.createMeterRegistry( + MetricsConfig.builder() + .globalTags(globalTags) + .build()); + + Counter counter1 = meterRegistry.getOrCreate(Counter.builder("a") + .tags(List.of(Tag.create("local1", "a")))); + assertThat("New counter's global tags", + counter1.id().tags(), + hasItems(globalTags.toArray(new Tag[0]))); + assertThat("New counter's original tags", + counter1.id().tags(), + hasItem(Tag.create("local1", "a"))); + } + + @Test + void testWithConfig() { + var settings = Map.of("metrics.tags", "g1=v1,g2=v2"); + + Config config = Config.just(ConfigSources.create(settings)); + + MeterRegistry meterRegistry = Metrics.createMeterRegistry( + MetricsConfig.create(config.get("metrics"))); + + Counter counter1 = meterRegistry.getOrCreate(Counter.builder("a") + .tags(List.of(Tag.create("local1", "a")))); + assertThat("New counter's global tags", + counter1.id().tags(), + hasItems(List.of(Tag.create("g1", "v1"), + Tag.create("g2", "v2")).toArray(new Tag[0]))); + assertThat("New counter's explicit tags", + counter1.id().tags(), + hasItem(Tag.create("local1", "a"))); + + } +} From 5a9cc92fb489dc10b648a5c362d259de17001b10 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 13 Aug 2023 01:08:22 -0500 Subject: [PATCH 24/40] Add common unit tests for implementations --- bom/pom.xml | 15 ++ metrics/pom.xml | 1 + metrics/providers/micrometer/pom.xml | 1 + .../metrics/micrometer/TestCounter.java | 31 ----- metrics/providers/pom.xml | 39 ++++++ metrics/testing/pom.xml | 55 ++++++++ .../testing}/SimpleMeterRegistryTests.java | 4 +- .../helidon/metrics/testing/TestCounter.java | 78 +++++++++++ .../metrics/testing}/TestDeletions.java | 2 +- .../testing}/TestDistributionSummary.java | 25 +--- .../metrics/testing}/TestGlobalTags.java | 2 +- .../java/io/helidon/metrics/testing/Util.java | 128 ++++++++++++++++++ .../helidon/metrics/testing/package-info.java | 20 +++ .../testing/src/main/java/module-info.java | 28 ++++ 14 files changed, 374 insertions(+), 55 deletions(-) create mode 100644 metrics/testing/pom.xml rename metrics/{providers/micrometer/src/test/java/io/helidon/metrics/micrometer => testing/src/main/java/io/helidon/metrics/testing}/SimpleMeterRegistryTests.java (95%) create mode 100644 metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java rename metrics/{providers/micrometer/src/test/java/io/helidon/metrics/micrometer => testing/src/main/java/io/helidon/metrics/testing}/TestDeletions.java (99%) rename metrics/{providers/micrometer/src/test/java/io/helidon/metrics/micrometer => testing/src/main/java/io/helidon/metrics/testing}/TestDistributionSummary.java (71%) rename metrics/{providers/micrometer/src/test/java/io/helidon/metrics/micrometer => testing/src/main/java/io/helidon/metrics/testing}/TestGlobalTags.java (98%) create mode 100644 metrics/testing/src/main/java/io/helidon/metrics/testing/Util.java create mode 100644 metrics/testing/src/main/java/io/helidon/metrics/testing/package-info.java create mode 100644 metrics/testing/src/main/java/module-info.java diff --git a/bom/pom.xml b/bom/pom.xml index af2bdc86589..0a361c68f99 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -374,6 +374,16 @@ helidon-metrics-api ${helidon.version}
+ + io.helidon.metrics + helidon-metrics-testing + ${helidon.version} + + + io.helidon.metrics + helidon-metrics-micrometer + ${helidon.version} + io.helidon.metrics helidon-metrics-service-api @@ -384,6 +394,11 @@ helidon-metrics-trace-exemplar ${helidon.version} + + io.helidon.tests.integration.metrics + helidon-tests-integration-metrics-common + ${helidon.version} + diff --git a/metrics/pom.xml b/metrics/pom.xml index f7475c08a2d..5532e74fbe9 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -36,5 +36,6 @@ trace-exemplar api service-api + testing
diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml index ed49517a6e2..fde4306eb87 100644 --- a/metrics/providers/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -59,4 +59,5 @@ test + diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java index 319faa3b754..3472fa7cdce 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java @@ -36,37 +36,6 @@ static void prep() { meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); } - @Test - void testIncr() { - Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); - assertThat("Initial counter value", c.count(), is(0D)); - c.increment(); - assertThat("After increment", c.count(), is(1D)); - }; - - @Test - void incrWithValue() { - Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); - assertThat("Initial counter value", c.count(), is(0D)); - c.increment(3D); - assertThat("After increment", c.count(), is(3D)); - } - - @Test - void incrBoth() { - Counter c = meterRegistry.getOrCreate(Counter.builder("c3")); - assertThat("Initial counter value", c.count(), is(0D)); - c.increment(2D); - assertThat("After increment", c.count(), is(2D)); - - Counter cAgain = meterRegistry.getOrCreate(Counter.builder("c3")); - assertThat("Looked up instance", cAgain, is(sameInstance(c))); - assertThat("Value after one update", cAgain.count(), is(2D)); - - cAgain.increment(3D); - assertThat("Value after second update", cAgain.count(), is(5D)); - } - @Test void testUnwrap() { Counter c = meterRegistry.getOrCreate(Counter.builder("c4")); diff --git a/metrics/providers/pom.xml b/metrics/providers/pom.xml index f156b501b7e..f3cd024cf1e 100644 --- a/metrics/providers/pom.xml +++ b/metrics/providers/pom.xml @@ -32,4 +32,43 @@ micrometer + + + + + io.helidon.metrics + helidon-metrics-testing + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + + + + + + + + + + + + io.helidon.metrics:helidon-metrics-testing + + + + + diff --git a/metrics/testing/pom.xml b/metrics/testing/pom.xml new file mode 100644 index 00000000000..71e9b21512c --- /dev/null +++ b/metrics/testing/pom.xml @@ -0,0 +1,55 @@ + + + + + 4.0.0 + + + io.helidon.metrics + helidon-metrics-project + 4.0.0-SNAPSHOT + ../pom.xml + + helidon-metrics-testing + Helidon Metrics Common Testing + + + + io.helidon.metrics + helidon-metrics-api + + + io.helidon.config + helidon-config + + + io.helidon.common.testing + helidon-common-testing-junit5 + + + org.junit.jupiter + junit-jupiter-api + + + org.hamcrest + hamcrest-all + + + + diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java similarity index 95% rename from metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java rename to metrics/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java index 87eac3e92df..eb6b92077d7 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/SimpleMeterRegistryTests.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.metrics.micrometer; +package io.helidon.metrics.testing; import java.util.List; @@ -44,7 +44,7 @@ static void prep() { @Test void testConflictingMetadata() { - Counter c1 = meterRegistry.getOrCreate(Counter.builder("b")); + meterRegistry.getOrCreate(Counter.builder("b")); assertThrows(IllegalArgumentException.class, () -> meterRegistry.getOrCreate(Timer.builder("b"))); diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java new file mode 100644 index 00000000000..757c5343d48 --- /dev/null +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.testing; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +class TestCounter { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testIncr() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(); + assertThat("After increment", c.count(), is(1D)); + } + + @Test + void incrWithValue() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(3D); + assertThat("After increment", c.count(), is(3D)); + } + + @Test + void incrBoth() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c3")); + assertThat("Initial counter value", c.count(), is(0D)); + c.increment(2D); + assertThat("After increment", c.count(), is(2D)); + + Counter cAgain = meterRegistry.getOrCreate(Counter.builder("c3")); + assertThat("Looked up instance", cAgain, is(sameInstance(c))); + assertThat("Value after one update", cAgain.count(), is(2D)); + + cAgain.increment(3D); + assertThat("Value after second update", cAgain.count(), is(5D)); + } + +// @Test +// void testUnwrap() { +// Counter c = meterRegistry.getOrCreate(Counter.builder("c4")); +// io.micrometer.core.instrument.Counter mCounter = c.unwrap(io.micrometer.core.instrument.Counter.class); +// assertThat("Initial value", c.count(), is(0D)); +// mCounter.increment(); +// assertThat("Updated value", c.count(), is(1D)); +// } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDeletions.java similarity index 99% rename from metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java rename to metrics/testing/src/main/java/io/helidon/metrics/testing/TestDeletions.java index 377fd5b4d02..359b231f2d2 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDeletions.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDeletions.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.metrics.micrometer; +package io.helidon.metrics.testing; import java.util.List; diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java similarity index 71% rename from metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java rename to metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java index 06915b31d6e..ac1a6f3fe03 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java @@ -13,19 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.metrics.micrometer; +package io.helidon.metrics.testing; import java.util.List; import java.util.concurrent.TimeUnit; -import io.helidon.metrics.api.CountAtBucket; import io.helidon.metrics.api.DistributionStatisticsConfig; import io.helidon.metrics.api.DistributionSummary; import io.helidon.metrics.api.HistogramSnapshot; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; import io.helidon.metrics.api.MetricsConfig; -import io.helidon.metrics.api.ValueAtPercentile; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -61,19 +59,6 @@ void testBasicStats() { assertThat("Total", summary.totalAmount(), is(16D)); } - @Test - void testUnwrap() { - DistributionSummary summary = commonPrep("b"); - io.micrometer.core.instrument.DistributionSummary s = summary.unwrap(io.micrometer.core.instrument.DistributionSummary.class); - assertThat("Initial count", s.count(), is(4L)); - s.record(9D); - assertThat("Count after native update", summary.count(), is(5L)); - assertThat("Total after native update", summary.totalAmount(), is(25D)); - - io.micrometer.core.instrument.distribution.HistogramSnapshot h = s.takeSnapshot(); - assertThat("Native snapshot mean", h.mean(), is(5D)); - } - @Test void testSnapshot() { DistributionSummary summary = commonPrep("c"); @@ -85,10 +70,10 @@ void testSnapshot() { @Test void testPercentiles() { - DistributionSummary summary = commonPrep("d"); - HistogramSnapshot snapshot = summary.snapshot(); +// DistributionSummary summary = commonPrep("d"); +// HistogramSnapshot snapshot = summary.snapshot(); - List vaps = Util.list(snapshot.percentileValues()); - List cabs = Util.list(snapshot.histogramCounts()); +// List vaps = Util.list(snapshot.percentileValues()); +// List cabs = Util.list(snapshot.histogramCounts()); } } diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGlobalTags.java similarity index 98% rename from metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java rename to metrics/testing/src/main/java/io/helidon/metrics/testing/TestGlobalTags.java index 26abdd8d1b8..ae08679f273 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGlobalTags.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGlobalTags.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.helidon.metrics.micrometer; +package io.helidon.metrics.testing; import java.util.List; import java.util.Map; diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/Util.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/Util.java new file mode 100644 index 00000000000..3ed1f148ffe --- /dev/null +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/Util.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.testing; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * Test utility code. + */ +public class Util { + + private Util() { + } + + /** + * Converts an iterable of double to a double array. + * + * @param iter iterable + * @return double array + */ + static double[] doubleArray(Iterable iter) { + List values = new ArrayList<>(); + iter.forEach(values::add); + double[] result = new double[values.size()]; + for (int i = 0; i < values.size(); i++) { + result[i] = values.get(i); + } + return result; + } + + /** + * Converts an interable of Double to a double array. + * + * @param iter iterable of Double + * @return double array + */ + static double[] doubleArray(Optional> iter) { + return iter.map(Util::doubleArray).orElse(null); + } + + /** + * Creates a new {@link java.util.List} from an {@link java.lang.Iterable}. + * + * @param iterable iterable to convert + * @return new list containing the elements reported by the iterable + * @param type of the items + */ + static List list(Iterable iterable) { + List result = new ArrayList<>(); + iterable.forEach(result::add); + return result; + } + + static Iterable iterable(double[] items) { + return items == null + ? null + : () -> new Iterator<>() { + + private int slot; + + @Override + public boolean hasNext() { + return slot < items.length; + } + + @Override + public Double next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return items[slot++]; + } + }; + } + +// static Iterable neutralTags(Iterable tags) { +// return () -> new Iterator<>() { +// +// private final Iterator tagsIter = tags.iterator(); +// +// @Override +// public boolean hasNext() { +// return tagsIter.hasNext(); +// } +// +// @Override +// public io.helidon.metrics.api.Tag next() { +// Tag next = tagsIter.next(); +// return io.helidon.metrics.api.Tag.create(next.getKey(), next.getValue()); +// } +// }; +// } + +// static Iterable tags(Iterable tags) { +// return () -> new Iterator<>() { +// +// private final Iterator tagsIter = tags.iterator(); +// +// @Override +// public boolean hasNext() { +// return tagsIter.hasNext(); +// } +// +// @Override +// public Tag next() { +// io.helidon.metrics.api.Tag next = tagsIter.next(); +// return Tag.of(next.key(), next.value()); +// } +// }; +// } +} diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/package-info.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/package-info.java new file mode 100644 index 00000000000..9dcef41e61e --- /dev/null +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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. + */ + +/** + * Shared unit tests and supporting code for unit tests of implementations of the metrics API. + */ +package io.helidon.metrics.testing; diff --git a/metrics/testing/src/main/java/module-info.java b/metrics/testing/src/main/java/module-info.java new file mode 100644 index 00000000000..3e541c846eb --- /dev/null +++ b/metrics/testing/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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. + */ + +/** + * Shared unit tests and support for metrics implementations. + */ +module helidon.metrics.testing { + + requires io.helidon.common.config; + requires io.helidon.metrics.api; + requires io.helidon.common.testing.junit5; + requires org.junit.jupiter.api; + requires hamcrest.all; + requires io.helidon.config; +} \ No newline at end of file From 05c4fec19df9d1385bc219a044bc009796a13605 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Sun, 13 Aug 2023 01:11:39 -0500 Subject: [PATCH 25/40] Remove commented unneeded lines; clarify comments --- metrics/providers/pom.xml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/metrics/providers/pom.xml b/metrics/providers/pom.xml index f3cd024cf1e..2905bb3a418 100644 --- a/metrics/providers/pom.xml +++ b/metrics/providers/pom.xml @@ -34,8 +34,8 @@ @@ -50,19 +50,6 @@ org.apache.maven.plugins maven-surefire-plugin - - - - - - - - - - - - - io.helidon.metrics:helidon-metrics-testing @@ -71,4 +58,5 @@ + From 5fa681bc35e940b69663065b60dae289aa769845 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 14 Aug 2023 12:28:39 -0500 Subject: [PATCH 26/40] Expand tests; corresponding changes to interfaces and implementations --- .../api/DistributionStatisticsConfig.java | 16 ++- .../io/helidon/metrics/api/NoOpMeter.java | 4 +- .../metrics/micrometer/MCountAtBucket.java | 25 +++++ .../MDistributionStatisticsConfig.java | 8 +- .../micrometer/MDistributionSummary.java | 8 ++ .../micrometer/MValueAtPercentile.java | 25 +++++ .../testing/TestDistributionSummary.java | 98 ++++++++++++++++--- 7 files changed, 157 insertions(+), 27 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index 2d6ae410d22..95152c40392 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -199,23 +199,19 @@ interface Builder extends Wrapped, io.helidon.common.Builder slos); + Builder buckets(Iterable buckets); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 404dbc7f442..849a0d76539 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -634,12 +634,12 @@ public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentilePre } @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder serviceLevelObjectives(double... slos) { + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder buckets(double... buckets) { return identity(); } @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder serviceLevelObjectives(Iterable slos) { + public io.helidon.metrics.api.DistributionStatisticsConfig.Builder buckets(Iterable buckets) { return identity(); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java index 2ac09ac148f..00d44853b27 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.micrometer; +import java.util.Objects; import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.distribution.CountAtBucket; @@ -50,4 +51,28 @@ public double count() { public R unwrap(Class c) { return c.cast(delegate); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + // Simplifies the use of test implementations in unit tests if equals does not insist that the other object + // also be a MCountAtBucket but merely implements CountAtBucket. + if (!(o instanceof io.helidon.metrics.api.CountAtBucket that)) { + return false; + } + return Objects.equals(delegate.bucket(), that.bucket()) + && Objects.equals(delegate.count(), that.count()); + } + + @Override + public int hashCode() { + return Objects.hash(delegate.bucket(), delegate.count()); + } + + @Override + public String toString() { + return String.format("MCountAtBucket[bucket=%f,count=%f]", bucket(), count()); + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java index 6e29ce36fba..1e1cfb09488 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java @@ -202,14 +202,14 @@ public Builder percentilePrecision(Integer digitsOfPrecision) { } @Override - public Builder serviceLevelObjectives(double... slos) { - delegate.serviceLevelObjectives(slos); + public Builder buckets(double... buckets) { + delegate.serviceLevelObjectives(buckets); return this; } @Override - public Builder serviceLevelObjectives(Iterable slos) { - delegate.serviceLevelObjectives(Util.doubleArray(slos)); + public Builder buckets(Iterable buckets) { + delegate.serviceLevelObjectives(Util.doubleArray(buckets)); return this; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index d486294c9e2..00139224082 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -75,8 +75,16 @@ private Builder(String name, DistributionStatisticsConfig.Builder configBuilder) } private Builder(String name, DistributionStatisticsConfig config) { super(DistributionSummary.builder(name) + .publishPercentiles(config.percentiles() + .map(Util::doubleArray) + .orElse(DEFAULT.getPercentiles())) .percentilePrecision(config.percentilePrecision() .orElse(DEFAULT.getPercentilePrecision())) + .publishPercentileHistogram(config.isPercentileHistogram() + .orElse(DEFAULT.isPercentileHistogram())) + .serviceLevelObjectives(config.serviceLevelObjectiveBoundaries() + .map(Util::doubleArray) + .orElse(DEFAULT.getServiceLevelObjectiveBoundaries())) .minimumExpectedValue(config.minimumExpectedValue() .orElse(DEFAULT.getMinimumExpectedValueAsDouble())) .maximumExpectedValue(config.maximumExpectedValue() diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java index ae9fc77796c..9259615b6e4 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.java @@ -15,6 +15,7 @@ */ package io.helidon.metrics.micrometer; +import java.util.Objects; import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.distribution.ValueAtPercentile; @@ -50,4 +51,28 @@ public double value(TimeUnit unit) { public R unwrap(Class c) { return c.cast(delegate); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + // Simplifies the use of test implementations in unit tests if equals does not insist that the other object + // also be a MValueAtPercentile but merely implements ValueAtPercentile. + if (!(o instanceof io.helidon.metrics.api.ValueAtPercentile that)) { + return false; + } + return Objects.equals(delegate.percentile(), that.percentile()) + && Objects.equals(delegate.value(), that.value()); + } + + @Override + public int hashCode() { + return Objects.hash(delegate.percentile(), delegate.value()); + } + + @Override + public String toString() { + return String.format("MValueAtPercentile[percentile=%f,value=%f]", percentile(), value()); + } } diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java index ac1a6f3fe03..c9b18fe438b 100644 --- a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java @@ -18,17 +18,21 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import io.helidon.metrics.api.CountAtBucket; import io.helidon.metrics.api.DistributionStatisticsConfig; import io.helidon.metrics.api.DistributionSummary; import io.helidon.metrics.api.HistogramSnapshot; import io.helidon.metrics.api.MeterRegistry; import io.helidon.metrics.api.Metrics; import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.ValueAtPercentile; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; class TestDistributionSummary { @@ -40,19 +44,18 @@ static void prep() { meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); } - private static DistributionSummary commonPrep(String name) { - DistributionStatisticsConfig.Builder statsConfigBuilder = DistributionStatisticsConfig.builder() - .percentilesHistogram(true) - .percentiles(0.5, 0.9, 0.99, 0.999); + private static DistributionSummary commonPrep(String name, DistributionStatisticsConfig.Builder statsConfigBuilder) { DistributionSummary summary = meterRegistry.getOrCreate(DistributionSummary.builder(name, statsConfigBuilder)); List.of(1D, 3D, 5D, 7D) .forEach(summary::record); return summary; } + @Test void testBasicStats() { - DistributionSummary summary = commonPrep("a"); + DistributionSummary summary = commonPrep("a", + DistributionStatisticsConfig.builder()); assertThat("Mean", summary.mean(), is(4D)); assertThat("Min", summary.max(), is(7D)); assertThat("Count", summary.count(), is(4L)); @@ -60,8 +63,9 @@ void testBasicStats() { } @Test - void testSnapshot() { - DistributionSummary summary = commonPrep("c"); + void testBasicSnapshot() { + DistributionSummary summary = commonPrep("c", + DistributionStatisticsConfig.builder()); HistogramSnapshot snapshot = summary.snapshot(); assertThat("Snapshot count", snapshot.count(), is(4L)); assertThat("Snapshot total", snapshot.total(), is(16D)); @@ -70,10 +74,82 @@ void testSnapshot() { @Test void testPercentiles() { -// DistributionSummary summary = commonPrep("d"); -// HistogramSnapshot snapshot = summary.snapshot(); + DistributionSummary summary = commonPrep("d", + DistributionStatisticsConfig.builder() + .percentiles(0.5, 0.9, 0.99, 0.999)); + HistogramSnapshot snapshot = summary.snapshot(); + + List vaps = Util.list(snapshot.percentileValues()); + + assertThat("Values at percentile", + vaps, + contains( + equalTo(Vap.create(0.50D, 3.0625D)), + equalTo(Vap.create(0.90D, 7.1875D)), + equalTo(Vap.create(0.99D, 7.1875D)), + equalTo(Vap.create(0.999D, 7.1875D)))); + } + + @Test + void testBuckets() { + DistributionSummary summary = commonPrep("e", + DistributionStatisticsConfig.builder() + .buckets(5.0D, 10.0D, 15.0D)); + + HistogramSnapshot snapshot = summary.snapshot(); + + List cabs = Util.list(snapshot.histogramCounts()); + + assertThat("Counts at buckets", + cabs, + contains( + equalTo(Cab.create(5.0D, 3.0D)), + equalTo(Cab.create(10.0D, 4.0D)), + equalTo(Cab.create(15.0D, 4.0D)))); + + } + + private record Vap(double percentile, double value) implements ValueAtPercentile { + + private static ValueAtPercentile create(double percentile, double value) { + return new Vap(percentile, value); + } + + @Override + public double value(TimeUnit unit) { + return unit.convert((long) value, TimeUnit.NANOSECONDS); + } + + @Override + public R unwrap(Class c) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return String.format("Vap[percentile=%f,value=%f]", percentile, value); + } + } + + private record Cab(double bucket, double count) implements CountAtBucket { + + private static Cab create(double bucket, double count) { + return new Cab(bucket, count); + } + + @Override + public double bucket(TimeUnit unit) { + return unit.convert((long) bucket, TimeUnit.NANOSECONDS); + } + + @Override + public R unwrap(Class c) { + throw new UnsupportedOperationException(); + } -// List vaps = Util.list(snapshot.percentileValues()); -// List cabs = Util.list(snapshot.histogramCounts()); + @Override + public String toString() { + return String.format("Vap[percentile=%f,value=%f]", bucket, count); + } } } From 10e13216be8c7819791a4810c8fe9a1eaec349f6 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 14 Aug 2023 14:47:43 -0500 Subject: [PATCH 27/40] Further gauge tests --- .../helidon/metrics/micrometer/TestGauge.java | 55 ++++++++++++ .../io/helidon/metrics/testing/TestGauge.java | 88 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGauge.java create mode 100644 metrics/testing/src/main/java/io/helidon/metrics/testing/TestGauge.java diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGauge.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGauge.java new file mode 100644 index 00000000000..b8a8059af23 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestGauge.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.concurrent.atomic.AtomicLong; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.Gauge; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class TestGauge { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testUnwrap() { + long initialValue = 4L; + long incr = 2L; + AtomicLong value = new AtomicLong(initialValue); + Gauge g = meterRegistry.getOrCreate(Gauge.builder("a", + value, + v -> (double)v.get())); + + io.micrometer.core.instrument.Gauge mGauge = g.unwrap(io.micrometer.core.instrument.Gauge.class); + assertThat("Initial value", mGauge.value(), is((double) initialValue)); + value.addAndGet(incr); + assertThat("Updated value", mGauge.value(), is((double) initialValue + incr)); + } +} diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGauge.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGauge.java new file mode 100644 index 00000000000..a30f2faf6c9 --- /dev/null +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGauge.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.testing; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.helidon.metrics.api.Gauge; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class TestGauge { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testGaugeAroundObject() { + + long initial = 4L; + long incr = 3L; + Custom c = new Custom(initial); + Gauge g = meterRegistry.getOrCreate(Gauge.builder("a", + c, + Custom::value)); + + assertThat("Gauge before update", g.value(), is((double) initial)); + + c.add(3L); + + assertThat("Gauge after update", g.value(), is((double) initial + incr)); + } + + @Test + void testGaugeWithLamdba() { + int initial = 11; + int incr = 4; + AtomicInteger i = new AtomicInteger(initial); + Gauge g = meterRegistry.getOrCreate(Gauge.builder("b", + i, + theInt -> (double) theInt.get())); + assertThat("Gauge before update", i.get(), is(initial)); + + i.getAndAdd(incr); + + assertThat("Gauge after update", g.value(), is((double) initial + incr)); + } + + private static class Custom { + + private long value; + + private Custom(long initialValue) { + value = initialValue; + } + + private void add(long delta) { + value += delta; + } + + private long value() { + return value; + } + } +} From 9cf45ec84f8e07eb42941b6d9cc97dda9873aa73 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 14 Aug 2023 14:51:18 -0500 Subject: [PATCH 28/40] Rebase: Adopt Tomas's changes for two builder classes rather than the interim fix he shared earlier --- .../java/io/helidon/builder/processor/PrototypeProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java b/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java index 07283b7b318..e7860a986f0 100644 --- a/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java +++ b/builder/processor/src/main/java/io/helidon/builder/processor/PrototypeProperty.java @@ -320,7 +320,7 @@ private static ConfiguredOption configuredFromAnnotation(TypeHandler typeHandler .map(typeHandler::toDefaultValue) .orElse(null), configuredAnnotation.getValue("builderMethod").map(Boolean::parseBoolean).orElse(true), - !configuredAnnotation.getValue("configured").map(Boolean::parseBoolean).orElse(true), + configuredAnnotation.getValue("notConfigured").map(Boolean::parseBoolean).orElse(false), provider, provider ? ProviderOption.create(configuredAnnotation) : null); } From 9e47f1bdf46ac2f4eace75d709a94635deed860c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 14 Aug 2023 15:57:12 -0500 Subject: [PATCH 29/40] Adapt to recent changes in support for config method in builder/generator --- .../helidon/metrics/api/MetricsConfigBlueprint.java | 9 ++++++--- .../io/helidon/metrics/api/MetricsConfigSupport.java | 11 +++++++++-- .../micrometer/MicrometerMetricsFactoryProvider.java | 6 ++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index 267e0046cc7..af57ea38b56 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -85,8 +85,12 @@ interface MetricsConfigBlueprint { @ConfiguredOption(key = APP_TAG_CONFIG_KEY) Optional appTagValue(); - @ConfiguredOption(builderMethod = false, configured = false) - Config metricsConfig(); + /** + * Metrics configuration node. + * + * @return metrics configuration + */ + Config config(); class BuilderDecorator implements Prototype.BuilderDecorator> { @@ -95,7 +99,6 @@ public void decorate(MetricsConfig.BuilderBase builder) { if (builder.config().isEmpty()) { builder.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); } - builder.metricsConfig(builder.config().get()); } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java index b481b3d5769..d5b6759b2c1 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java @@ -22,15 +22,22 @@ class MetricsConfigSupport { + /** + * Looks up a single config value within the metrics configuration by config key. + * + * @param metricsConfig the {@link io.helidon.common.config.Config} node containing the metrics configuration + * @param key config key to fetch + * @return config value + */ @Prototype.PrototypeMethod static Optional lookupConfig(MetricsConfig metricsConfig, String key) { - return metricsConfig.metricsConfig() + return metricsConfig.config() .get(key) .asString() .asOptional(); } - // Pattern: + // Pattern of a single tag assignment (tag=value): // - capture reluctant match of anything // - non-capturing match of an unescaped = // - capture the rest. diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java index cdd6de08558..80ef16bf700 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java @@ -56,6 +56,12 @@ public MetricsFactory create(MetricsConfig metricsConfig) { return MicrometerMetricsFactory.create(metricsConfig); } + /** + * Creates a new {@link io.helidon.metrics.api.MetricsFactory} based on Micrometer. + */ + public MicrometerMetricsFactoryProvider() { + } + private MeterRegistry getRegistry() { return Metrics.globalRegistry; } From 310aeebfcfbfd2ca8bd574e50fa2376c9ba1d0f8 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Mon, 14 Aug 2023 17:42:29 -0500 Subject: [PATCH 30/40] Some simplification of the API; use longs not doubles for counts, etc. --- .../io/helidon/metrics/api/CountAtBucket.java | 2 +- .../java/io/helidon/metrics/api/Counter.java | 4 +- .../api/DistributionStatisticsConfig.java | 102 ++---------------- .../io/helidon/metrics/api/NoOpMeter.java | 66 +----------- .../metrics/micrometer/MCountAtBucket.java | 10 +- .../helidon/metrics/micrometer/MCounter.java | 13 +-- .../MDistributionStatisticsConfig.java | 96 +---------------- .../micrometer/MDistributionSummary.java | 24 +---- .../io/helidon/metrics/micrometer/MGauge.java | 8 +- .../io/helidon/metrics/micrometer/MTimer.java | 14 +-- .../metrics/micrometer/TestCounter.java | 4 +- .../helidon/metrics/testing/TestCounter.java | 36 +++---- .../testing/TestDistributionSummary.java | 12 +-- 13 files changed, 60 insertions(+), 331 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java index 11e53ebacf9..28a3b5380ac 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java @@ -47,5 +47,5 @@ public interface CountAtBucket extends Wrapped { * * @return observation count for the bucket */ - double count(); + long count(); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java index b5dd40b485b..c80130ed4be 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.java @@ -41,14 +41,14 @@ static Builder builder(String name) { * * @param amount amount to add to the counter. */ - void increment(double amount); + void increment(long amount); /** * Returns the cumulative count since this counter was registered. * * @return cumulative count since this counter was registered */ - double count(); + long count(); /** * Builder for a new counter. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index 95152c40392..082d217dd8f 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -15,7 +15,6 @@ */ package io.helidon.metrics.api; -import java.time.Duration; import java.util.Optional; /** @@ -34,36 +33,6 @@ static Builder builder() { return MetricsFactory.getInstance().distributionStatisticsConfigBuilder(); } - /** - * Creates a new configuration by merging another one (called the "parent") with the current instance, - * using values from the current instance if they have been set and from the parent otherwise. - * - * @param parent the other configuration - * @return new config resulting from the merge - */ - DistributionStatisticsConfig merge(DistributionStatisticsConfig parent); - - /** - * Returns whether the configuration is set for percentile histograms which can be aggregated for percentile approximations. - * - * @return whether percentile histograms are configured - */ - Optional isPercentileHistogram(); - - /** - * Returns whether the configuration is set to publish percentiles. - * - * @return true/false - */ - Optional isPublishingPercentiles(); - - /** - * Returns whether the configuration is set to publish a histogram. - * - * @return true/false - */ - Optional isPublishingHistogram(); - /** * Returns the settings for non-aggregable percentiles. * @@ -71,13 +40,6 @@ static Builder builder() { */ Optional> percentiles(); - /** - * Returns the configured number of digits of precision for percentiles. - * - * @return digits of precision to maintain for percentile approximations - */ - Optional percentilePrecision(); - /** * Returns the minimum expected value that the meter is expected to observe. * @@ -93,56 +55,17 @@ static Builder builder() { Optional maximumExpectedValue(); /** - * Returns how long decaying past observations remain in the ring buffer. - * - * @see #bufferLength() - * @return time during which samples accumulate in a histogram - */ - Optional expiry(); - - /** - * Returns the size of the ring buffer for holding decaying observations. + * Returns the configured bucket boundaries. * - * @return number of observations to keep in the ring buffer + * @return the bucket boundaries */ - Optional bufferLength(); - - /** - * Returns the configured service level objective boundaries. - * - * @return the SLO boundaries - */ - Optional> serviceLevelObjectiveBoundaries(); + Optional> buckets(); /** * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. */ interface Builder extends Wrapped, io.helidon.common.Builder { - /** - * Sets how long to keep samples before they are assumed to have decayed to zero and are discareded. - * - * @param expiry how long to retain samples - * @return updated builder - */ - Builder expiry(Duration expiry); - - /** - * Sets the size of the ring buffer which holds saved samples as they decay. - * - * @param bufferLength number of histograms to keep in the ring buffer - * @return updated builder - */ - Builder bufferLength(Integer bufferLength); - - /** - * Sets whether to publish percentiles histograms (which are aggregable). - * - * @param enabled true to publish percentile histograms; false otherwise - * @return updated builder - */ - Builder percentilesHistogram(Boolean enabled); - /** * Sets the minimum value that the meter is expected to observe. * @@ -160,11 +83,10 @@ interface Builder extends Wrapped, io.helidon.common.Builder * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed - * elsewhere. In contrast, a percentile histogram triggered by invoking {@link #percentilesHistogram} can - * be aggregated. + * elsewhere. *

*

* Specify percentiles a decimals, for example express the 95th percentile as {@code 0.95}. @@ -175,11 +97,10 @@ interface Builder extends Wrapped, io.helidon.common.Builder * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed - * elsewhere. In contrast, a percentile histogram triggered by invoking {@link #percentilesHistogram} can - * be aggregated. + * elsewhere. *

*

* Specify percentiles a decimals, for example express the 95th percentile as {@code 0.95}. @@ -189,15 +110,6 @@ interface Builder extends Wrapped, io.helidon.common.Builder percentiles); - /** - * Sets the number of digits of precision to maintain on the dynamic range - * histogram used to compute percentile approximations. - * - * @param digitsOfPrecision digits of precision to maintain for percentile approximations - * @return updated builder - */ - Builder percentilePrecision(Integer digitsOfPrecision); - /** * Sets the bucket boundaries. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 849a0d76539..7b185a2d084 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -228,12 +228,12 @@ public void increment() { } @Override - public void increment(double amount) { + public void increment(long amount) { } @Override - public double count() { - return 0; + public long count() { + return 0L; } } @@ -261,7 +261,7 @@ public void increment() { } @Override - public void increment(double amount) { + public void increment(long amount) { throw new UnsupportedOperationException(); } } @@ -593,21 +593,6 @@ public io.helidon.metrics.api.DistributionStatisticsConfig build() { return new NoOpMeter.DistributionStatisticsConfig(this); } - @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder expiry(Duration expiry) { - return identity(); - } - - @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder bufferLength(Integer bufferLength) { - return identity(); - } - - @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentilesHistogram(Boolean enabled) { - return identity(); - } - @Override public io.helidon.metrics.api.DistributionStatisticsConfig.Builder minimumExpectedValue(Double min) { return identity(); @@ -628,11 +613,6 @@ public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentiles(I return identity(); } - @Override - public io.helidon.metrics.api.DistributionStatisticsConfig.Builder percentilePrecision(Integer digitsOfPrecision) { - return identity(); - } - @Override public io.helidon.metrics.api.DistributionStatisticsConfig.Builder buckets(double... buckets) { return identity(); @@ -647,37 +627,11 @@ public io.helidon.metrics.api.DistributionStatisticsConfig.Builder buckets(Itera private DistributionStatisticsConfig(DistributionStatisticsConfig.Builder builder) { } - @Override - public io.helidon.metrics.api.DistributionStatisticsConfig merge( - io.helidon.metrics.api.DistributionStatisticsConfig parent) { - return builder().build(); - } - - @Override - public Optional isPercentileHistogram() { - return Optional.empty(); - } - - @Override - public Optional isPublishingPercentiles() { - return Optional.empty(); - } - - @Override - public Optional isPublishingHistogram() { - return Optional.empty(); - } - @Override public Optional> percentiles() { return Optional.empty(); } - @Override - public Optional percentilePrecision() { - return Optional.empty(); - } - @Override public Optional minimumExpectedValue() { return Optional.empty(); @@ -689,17 +643,7 @@ public Optional maximumExpectedValue() { } @Override - public Optional expiry() { - return Optional.empty(); - } - - @Override - public Optional bufferLength() { - return Optional.empty(); - } - - @Override - public Optional> serviceLevelObjectiveBoundaries() { + public Optional> buckets() { return Optional.empty(); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java index 00d44853b27..d7a77916de5 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java @@ -43,8 +43,8 @@ public double bucket(TimeUnit unit) { } @Override - public double count() { - return delegate.count(); + public long count() { + return (long) delegate.count(); } @Override @@ -63,16 +63,16 @@ public boolean equals(Object o) { return false; } return Objects.equals(delegate.bucket(), that.bucket()) - && Objects.equals(delegate.count(), that.count()); + && Objects.equals((long) delegate.count(), that.count()); } @Override public int hashCode() { - return Objects.hash(delegate.bucket(), delegate.count()); + return Objects.hash((long) delegate.bucket(), delegate.count()); } @Override public String toString() { - return String.format("MCountAtBucket[bucket=%f,count=%f]", bucket(), count()); + return String.format("MCountAtBucket[bucket=%f,count=%d]", bucket(), count()); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java index 620075daf1b..ed5a36212fa 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.java @@ -37,13 +37,13 @@ public void increment() { } @Override - public void increment(double amount) { + public void increment(long amount) { delegate().increment(amount); } @Override - public double count() { - return delegate().count(); + public long count() { + return (long) delegate().count(); } static class Builder extends MMeter.Builder @@ -55,12 +55,5 @@ private Builder(String name) { delegate()::description, delegate()::baseUnit); } - - // TODO remove if truly not used -// @Override -// MCounter register(MMeterRegistry mMeterRegistry) { -// -// return MCounter.create(delegate().register(mMeterRegistry)); -// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java index 1e1cfb09488..bfd7a6d8def 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java @@ -15,7 +15,6 @@ */ package io.helidon.metrics.micrometer; -import java.time.Duration; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -48,64 +47,11 @@ private MDistributionStatisticsConfig(DistributionStatisticConfig delegate) { this.delegate = delegate; } - @Override - public MDistributionStatisticsConfig merge(io.helidon.metrics.api.DistributionStatisticsConfig parent) { - DistributionStatisticConfig newDelegate = DistributionStatisticConfig.builder() - .percentilesHistogram( - chooseOpt(delegate.isPercentileHistogram(), - parent::isPercentileHistogram)) - .percentiles( - choose(delegate.getPercentiles(), - () -> Util.doubleArray(parent.percentiles()))) - .serviceLevelObjectives( - choose(delegate.getServiceLevelObjectiveBoundaries(), - () -> Util.doubleArray(parent.serviceLevelObjectiveBoundaries()))) - .percentilePrecision( - chooseOpt(delegate.getPercentilePrecision(), - parent::percentilePrecision)) - .minimumExpectedValue( - chooseOpt(delegate.getMinimumExpectedValueAsDouble(), - parent::minimumExpectedValue)) - .maximumExpectedValue( - chooseOpt(delegate.getMaximumExpectedValueAsDouble(), - parent::maximumExpectedValue)) - .expiry( - chooseOpt(delegate.getExpiry(), - parent::expiry)) - .bufferLength( - chooseOpt(delegate.getBufferLength(), - parent::bufferLength)) - .build(); - return new MDistributionStatisticsConfig(newDelegate); - } - - - - @Override - public Optional isPercentileHistogram() { - return Optional.ofNullable(delegate.isPercentileHistogram()); - } - - @Override - public Optional isPublishingPercentiles() { - return Optional.of(delegate.isPublishingPercentiles()); - } - - @Override - public Optional isPublishingHistogram() { - return Optional.of(delegate.isPublishingHistogram()); - } - @Override public Optional> percentiles() { return Optional.ofNullable(Util.iterable(delegate.getPercentiles())); } - @Override - public Optional percentilePrecision() { - return Optional.ofNullable(delegate.getPercentilePrecision()); - } - @Override public Optional minimumExpectedValue() { return Optional.ofNullable(delegate.getMinimumExpectedValueAsDouble()); @@ -117,17 +63,7 @@ public Optional maximumExpectedValue() { } @Override - public Optional expiry() { - return Optional.ofNullable(delegate.getExpiry()); - } - - @Override - public Optional bufferLength() { - return Optional.ofNullable(delegate.getBufferLength()); - } - - @Override - public Optional> serviceLevelObjectiveBoundaries() { + public Optional> buckets() { return Optional.ofNullable(Util.iterable(delegate.getServiceLevelObjectiveBoundaries())); } @@ -153,24 +89,6 @@ public MDistributionStatisticsConfig build() { return new MDistributionStatisticsConfig(this); } - @Override - public Builder expiry(Duration expiry) { - delegate.expiry(expiry); - return this; - } - - @Override - public Builder bufferLength(Integer bufferLength) { - delegate.bufferLength(bufferLength); - return this; - } - - @Override - public Builder percentilesHistogram(Boolean enabled) { - delegate.percentilesHistogram(enabled); - return this; - } - @Override public Builder minimumExpectedValue(Double min) { delegate.minimumExpectedValue(min); @@ -195,12 +113,6 @@ public Builder percentiles(Iterable percentiles) { return this; } - @Override - public Builder percentilePrecision(Integer digitsOfPrecision) { - delegate.percentilePrecision(digitsOfPrecision); - return this; - } - @Override public Builder buckets(double... buckets) { delegate.serviceLevelObjectives(buckets); @@ -227,10 +139,4 @@ static T chooseOpt(T fromChild, Supplier> fromParent) { return Objects.requireNonNullElseGet(fromChild, () -> fromParent.get().orElse(null)); } - - static T choose(T fromChild, Supplier fromParent) { - return Objects.requireNonNullElseGet(fromChild, fromParent); - } - - } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index 00139224082..aba5d1cca43 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -78,21 +78,13 @@ private Builder(String name, DistributionStatisticsConfig config) { .publishPercentiles(config.percentiles() .map(Util::doubleArray) .orElse(DEFAULT.getPercentiles())) - .percentilePrecision(config.percentilePrecision() - .orElse(DEFAULT.getPercentilePrecision())) - .publishPercentileHistogram(config.isPercentileHistogram() - .orElse(DEFAULT.isPercentileHistogram())) - .serviceLevelObjectives(config.serviceLevelObjectiveBoundaries() + .serviceLevelObjectives(config.buckets() .map(Util::doubleArray) .orElse(DEFAULT.getServiceLevelObjectiveBoundaries())) .minimumExpectedValue(config.minimumExpectedValue() .orElse(DEFAULT.getMinimumExpectedValueAsDouble())) .maximumExpectedValue(config.maximumExpectedValue() - .orElse(DEFAULT.getMaximumExpectedValueAsDouble())) - .distributionStatisticExpiry(config.expiry() - .orElse(DEFAULT.getExpiry())) - .distributionStatisticBufferLength(config.bufferLength() - .orElse(DEFAULT.getBufferLength()))); + .orElse(DEFAULT.getMaximumExpectedValueAsDouble()))); } @Override @@ -108,21 +100,11 @@ public Builder distributionStatisticsConfig( DistributionSummary.Builder delegate = delegate(); config.percentiles().ifPresent(p -> delegate.publishPercentiles(Util.doubleArray(p))); - config.percentilePrecision().ifPresent(delegate::percentilePrecision); - config.isPercentileHistogram().ifPresent(delegate::publishPercentileHistogram); - config.serviceLevelObjectiveBoundaries().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos))); + config.buckets().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos))); config.minimumExpectedValue().ifPresent(delegate::minimumExpectedValue); config.maximumExpectedValue().ifPresent(delegate::maximumExpectedValue); - config.expiry().ifPresent(delegate::distributionStatisticExpiry); - config.bufferLength().ifPresent(delegate::distributionStatisticBufferLength); return identity(); } - - // TODO remove if not used -// @Override -// MDistributionSummary register(MeterRegistry meterRegistry) { -// return MDistributionSummary.create(delegate().register(meterRegistry)); -// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java index 08400ebf5a7..56e3c04f561 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.java @@ -40,17 +40,11 @@ public double value() { static class Builder extends MMeter.Builder, MGauge.Builder, MGauge> implements io.helidon.metrics.api.Gauge.Builder { - private Builder(String name, T stateObject, ToDoubleFunction fn) { + private Builder(String name, T stateObject, ToDoubleFunction fn) { super(Gauge.builder(name, stateObject, fn)); prep(delegate()::tags, delegate()::description, delegate()::baseUnit); } - - // TODO remove if not used -// @Override -// MGauge register(MeterRegistry meterRegistry) { -// return MGauge.create(delegate().register(meterRegistry)); -// } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 31ab524f9d4..8e70248b2de 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -146,6 +146,14 @@ public double max(TimeUnit unit) { return delegate().max(unit); } + @Override + public String toString() { + TimeUnit baseTimeUnit = delegate().baseTimeUnit(); + return String.format("MTimer[count=%d,totalTime=%s]", delegate().count(), + Duration.of((long) delegate().totalTime(baseTimeUnit), + baseTimeUnit.toChronoUnit())); + } + static class Builder extends MMeter.Builder implements io.helidon.metrics.api.Timer.Builder { @@ -153,12 +161,6 @@ private Builder(String name) { super(Timer.builder(name)); } - // TODO remove if not used -// @Override -// MTimer register(MeterRegistry meterRegistry) { -// return MTimer.create(delegate().register(meterRegistry)); -// } - @Override public Builder publishPercentiles(double... percentiles) { delegate().publishPercentiles(percentiles); diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java index 3472fa7cdce..0cbf66e7aa8 100644 --- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java @@ -40,8 +40,8 @@ static void prep() { void testUnwrap() { Counter c = meterRegistry.getOrCreate(Counter.builder("c4")); io.micrometer.core.instrument.Counter mCounter = c.unwrap(io.micrometer.core.instrument.Counter.class); - assertThat("Initial value", c.count(), is(0D)); + assertThat("Initial value", c.count(), is(0L)); mCounter.increment(); - assertThat("Updated value", c.count(), is(1D)); + assertThat("Updated value", c.count(), is(1L)); } } diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java index 757c5343d48..15cab606176 100644 --- a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.java @@ -39,40 +39,36 @@ static void prep() { @Test void testIncr() { Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); - assertThat("Initial counter value", c.count(), is(0D)); + assertThat("Initial counter value", c.count(), is(0L)); c.increment(); - assertThat("After increment", c.count(), is(1D)); + assertThat("After increment", c.count(), is(1L)); } @Test void incrWithValue() { Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); - assertThat("Initial counter value", c.count(), is(0D)); - c.increment(3D); - assertThat("After increment", c.count(), is(3D)); + assertThat("Initial counter value", c.count(), is(0L)); + c.increment(3L); + assertThat("After increment", c.count(), is(3L)); } @Test void incrBoth() { + long initialValue = 0; + long incr = 2L; Counter c = meterRegistry.getOrCreate(Counter.builder("c3")); - assertThat("Initial counter value", c.count(), is(0D)); - c.increment(2D); - assertThat("After increment", c.count(), is(2D)); + assertThat("Initial counter value", c.count(), is(initialValue)); + c.increment(incr); + assertThat("After increment", c.count(), is(initialValue + incr)); + + initialValue += incr; + incr = 3L; Counter cAgain = meterRegistry.getOrCreate(Counter.builder("c3")); assertThat("Looked up instance", cAgain, is(sameInstance(c))); - assertThat("Value after one update", cAgain.count(), is(2D)); + assertThat("Value after one update", cAgain.count(), is(initialValue)); - cAgain.increment(3D); - assertThat("Value after second update", cAgain.count(), is(5D)); + cAgain.increment(incr); + assertThat("Value after second update", cAgain.count(), is(initialValue + incr)); } - -// @Test -// void testUnwrap() { -// Counter c = meterRegistry.getOrCreate(Counter.builder("c4")); -// io.micrometer.core.instrument.Counter mCounter = c.unwrap(io.micrometer.core.instrument.Counter.class); -// assertThat("Initial value", c.count(), is(0D)); -// mCounter.increment(); -// assertThat("Updated value", c.count(), is(1D)); -// } } diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java index c9b18fe438b..d2329d54bb5 100644 --- a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java @@ -103,9 +103,9 @@ void testBuckets() { assertThat("Counts at buckets", cabs, contains( - equalTo(Cab.create(5.0D, 3.0D)), - equalTo(Cab.create(10.0D, 4.0D)), - equalTo(Cab.create(15.0D, 4.0D)))); + equalTo(Cab.create(5.0D, 3)), + equalTo(Cab.create(10.0D, 4)), + equalTo(Cab.create(15.0D, 4)))); } @@ -131,9 +131,9 @@ public String toString() { } } - private record Cab(double bucket, double count) implements CountAtBucket { + private record Cab(double bucket, long count) implements CountAtBucket { - private static Cab create(double bucket, double count) { + private static Cab create(double bucket, long count) { return new Cab(bucket, count); } @@ -149,7 +149,7 @@ public R unwrap(Class c) { @Override public String toString() { - return String.format("Vap[percentile=%f,value=%f]", bucket, count); + return String.format("Vap[bucket=%f,count=%d]", bucket, count); } } } From 40a9b5bf18b41020e2f9b6d6b275c003512ef9cd Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 01:14:54 -0500 Subject: [PATCH 31/40] Add tests; fix some timer/sample problems --- .../java/io/helidon/metrics/api/Clock.java | 12 + .../metrics/api/DistributionSummary.java | 14 +- .../io/helidon/metrics/api/MeterRegistry.java | 7 + .../helidon/metrics/api/MetricsFactory.java | 12 +- .../io/helidon/metrics/api/NoOpMeter.java | 29 +- .../metrics/api/NoOpMeterRegistry.java | 16 +- .../metrics/api/NoOpMetricsFactory.java | 5 + .../java/io/helidon/metrics/api/Timer.java | 51 +--- .../io/helidon/metrics/micrometer/MClock.java | 13 +- .../metrics/micrometer/MMeterRegistry.java | 108 +++++++- .../io/helidon/metrics/micrometer/MTimer.java | 60 ++--- .../micrometer/MicrometerMetricsFactory.java | 5 + .../micrometer/TestDistributionSummary.java | 60 +++++ .../helidon/metrics/micrometer/TestTimer.java | 67 +++++ .../io/helidon/metrics/testing/TestTimer.java | 248 ++++++++++++++++++ 15 files changed, 574 insertions(+), 133 deletions(-) create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestTimer.java create mode 100644 metrics/testing/src/main/java/io/helidon/metrics/testing/TestTimer.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java index 77de260b350..3b3f746d06c 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java @@ -57,4 +57,16 @@ static Clock system() { * @return monotonic time in nanoseconds */ long monotonicTime(); + + /** + * Unwraps the clock to the specified type (typically not needed for custom clocks). + * + * @param c {@link Class} to which to cast this object + * @return unwrapped clock + * @param the type of the unwrapped clock + */ + @Override + default R unwrap(Class c) { + throw new UnsupportedOperationException(); + } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java index 35c3898ab57..a6e77f5f0fe 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java @@ -21,7 +21,7 @@ public interface DistributionSummary extends Meter { /** - * Creates a builder for a new {@link io.helidon.metrics.api.DistributionSummary}. + * Creates a builder for a new {@link io.helidon.metrics.api.DistributionSummary} using the specified statistics builder. * * @param name name for the summary * @param configBuilder distribution stats config for the summary @@ -33,6 +33,18 @@ static Builder builder(String name, .distributionSummaryBuilder(name, configBuilder); } + /** + * Creates a builder for a new {@link io.helidon.metrics.api.DistributionSummary} using default distribution statistics. + * + * @param name name for the summary + * @return new builder + */ + static Builder builder(String name) { + return MetricsFactory.getInstance() + .distributionSummaryBuilder(name, + DistributionStatisticsConfig.builder()); + } + /** * Updates the statistics kept by the summary with the specified amount. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 749697c2a3a..95bdc5182a0 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -40,6 +40,13 @@ public interface MeterRegistry extends Wrapped { */ Collection meters(Predicate filter); + /** + * Returns the default {@link io.helidon.metrics.api.Clock} in use by the registry. + * + * @return default clock + */ + Clock clock(); + /** * Locates a previously-registered meter using the name and tags in the provided builder or, if not found, registers a new * one using the provided builder. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index 8596ebbd8e3..d3dd1196f80 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -60,13 +60,23 @@ static MetricsFactory getInstance() { MeterRegistry globalRegistry(); /** - * Creates a new {@link MeterRegistry} using the provided metrics config. + * Creates a new {@link io.helidon.metrics.api.MeterRegistry} using the provided metrics config. * * @param metricsConfig metrics configuration which influences the new registry * @return new meter registry */ MeterRegistry createMeterRegistry(MetricsConfig metricsConfig); + /** + * Creates a new {@link io.helidon.metrics.api.MeterRegistry} using the provided {@link io.helidon.metrics.api.Clock} and + * metrics config. + * + * @param clock default clock to associate with the meter registry + * @param metricsConfig metrics configuration which influences the new registry + * @return new meter registry + */ + MeterRegistry createMeterRegistry(Clock clock, MetricsConfig metricsConfig); + /** * Returns the system {@link io.helidon.metrics.api.Clock} from the * underlying metrics implementation. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 7b185a2d084..423aac96003 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -461,27 +461,12 @@ public Timer build() { } @Override - public Builder publishPercentiles(double... percentiles) { + public Builder percentiles(double... percentiles) { return identity(); } @Override - public Builder percentilePrecision(Integer digitsOfPrecision) { - return identity(); - } - - @Override - public Builder publishPercentileHistogram() { - return identity(); - } - - @Override - public Builder publishPercentileHistogram(Boolean enabled) { - return identity(); - } - - @Override - public Builder serviceLevelObjectives(Duration... slos) { + public Builder buckets(Duration... buckets) { return identity(); } @@ -494,16 +479,6 @@ public Builder minimumExpectedValue(Duration min) { public Builder maximumExpectedValue(Duration max) { return identity(); } - - @Override - public Builder distributionStatisticExpiry(Duration expiry) { - return identity(); - } - - @Override - public Builder distributionStatisticBufferLength(Integer bufferLength) { - return identity(); - } } static Builder builder(String name) { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 6b235e8c8a0..a59b8a9750c 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -17,29 +17,22 @@ import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; /** * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}. *

- * Note that the no-op meter registry implement does not actually + * Note that the no-op meter registry implementation does not actually * store meters or their IDs, in line with the documented behavior of disabled metrics. *

*/ class NoOpMeterRegistry implements MeterRegistry, NoOpWrapped { - private final Map meters = new ConcurrentHashMap<>(); - - private final ReentrantLock metersAccess = new ReentrantLock(); - @Override public List meters() { - return List.of(meters.values().toArray(new Meter[0])); + return List.of(); } @Override @@ -47,6 +40,11 @@ public Collection meters(Predicate filter) { return Set.of(); } + @Override + public Clock clock() { + return Clock.system(); + } + @Override public Optional remove(Meter.Id id) { return Optional.empty(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java index 655deb4df74..a7ec5a19884 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java @@ -55,6 +55,11 @@ public MeterRegistry createMeterRegistry(MetricsConfig metricsConfig) { return new NoOpMeterRegistry(); } + @Override + public MeterRegistry createMeterRegistry(Clock clock, MetricsConfig metricsConfig) { + return createMeterRegistry(metricsConfig); + } + @Override public Clock clockSystem() { return SYSTEM_CLOCK; diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index b2378229552..3cc4eecf29e 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -197,44 +197,19 @@ interface Builder extends Meter.Builder { * @param percentiles percentiles to compute and publish * @return updated builder */ - Builder publishPercentiles(double... percentiles); + Builder percentiles(double... percentiles); /** - * Sets the precision for computing histogram percentile approximations. + * Sets the bucket boundaries. * - * @param digitsOfPrecision number of digits of precision + * @param buckets bucket boundaries * @return updated builder */ - Builder percentilePrecision(Integer digitsOfPrecision); - - /** - * Sets to add histogram buckets. - *

- * Equivalent to {@code publishPercentilHistogram(true)}). - *

- * - * @return updated builder - */ - Builder publishPercentileHistogram(); - - /** - * Sets whether to add histogram buckets. - * - * @param enabled true/false - * @return updated builder - */ - Builder publishPercentileHistogram(Boolean enabled); - - /** - * Sets the service level objectives, guaranteeing at least those buckets in the histogram. - * - * @param slos service-level objective bucket boundaries - * @return updated builder - */ - Builder serviceLevelObjectives(Duration... slos); + Builder buckets(Duration... buckets); /** * Sets the minimum expected value the timer is expected to record. + * * @param min minimum expected value * @return updated builder */ @@ -247,21 +222,5 @@ interface Builder extends Meter.Builder { * @return updated builder */ Builder maximumExpectedValue(Duration max); - - /** - * Sets how long age-decayed samples are retained in ring buffers for use in the timer histograms. - * - * @param expiry amount of time to keep samples - * @return updated builder - */ - Builder distributionStatisticExpiry(Duration expiry); - - /** - * Sets the size of the ring buffer for retaining samples for histograms. - * - * @param bufferLength size of the ring buffer to use - * @return updated builder - */ - Builder distributionStatisticBufferLength(Integer bufferLength); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java index d49c621ee73..01777b0addb 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java @@ -15,18 +15,21 @@ */ package io.helidon.metrics.micrometer; +import io.micrometer.core.instrument.Clock; + /** - * + * Wrapper for a {@link io.micrometer.core.instrument.Clock} when one is returned from + * Micrometer. */ class MClock implements io.helidon.metrics.api.Clock { - static MClock create(io.micrometer.core.instrument.Clock delegate) { + static MClock create(Clock delegate) { return new MClock(delegate); } - private final io.micrometer.core.instrument.Clock delegate; + private final Clock delegate; - private MClock(io.micrometer.core.instrument.Clock delegate) { + private MClock(Clock delegate) { this.delegate = delegate; } @Override @@ -39,7 +42,7 @@ public long monotonicTime() { return delegate.monotonicTime(); } - io.micrometer.core.instrument.Clock delegate() { + Clock delegate() { return delegate; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 6f7cfff8e0c..57a1254327b 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -21,6 +21,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; +import io.helidon.metrics.api.Clock; import io.helidon.metrics.api.MetricsConfig; import io.micrometer.core.instrument.Counter; @@ -39,24 +40,94 @@ class MMeterRegistry implements io.helidon.metrics.api.MeterRegistry { private static final System.Logger LOGGER = System.getLogger(MMeterRegistry.class.getName()); + /** + * Creates a new meter registry which wraps the specified Micrometer meter registry, ensuring that if + * the meter registry is a composite registry it has a Prometheus meter registry attached (adding a new one if needed). + *

+ * The {@link io.helidon.metrics.api.MetricsConfig} does not override the settings of the pre-existing Micrometer + * meter registry but augments the behavior of this wrapper around it, for example specifying + * global tags. + *

+ * + * @param meterRegistry existing Micrometer meter registry to wrap + * @param metricsConfig metrics config + * @return new wrapper around the specified Micrometer meter registry + */ static MMeterRegistry create(MeterRegistry meterRegistry, MetricsConfig metricsConfig) { - return new MMeterRegistry(meterRegistry, metricsConfig); + // The caller passed a pre-existing meter registry, with its own clock, so wrap that clock + // with a Helidon clock adapter (MClock). + return new MMeterRegistry(ensurePrometheusRegistryIsPresent(meterRegistry, metricsConfig), + MClock.create(meterRegistry.config().clock()), + metricsConfig); } + /** + * Creates a new meter registry which wraps an automatically-created new Micrometer + * {@link io.micrometer.core.instrument.composite.CompositeMeterRegistry} with a Prometheus meter registry + * automatically added. + * + * @param metricsConfig metrics config + * @return new wrapper around a new Micrometer composite meter registry + */ static MMeterRegistry create(MetricsConfig metricsConfig) { CompositeMeterRegistry delegate = new CompositeMeterRegistry(); - delegate.add(new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(null))); - return new MMeterRegistry(delegate, metricsConfig); + return create(ensurePrometheusRegistryIsPresent(delegate, metricsConfig), + MClock.create(delegate.config().clock()), + metricsConfig); + } + + /** + * Creates a new meter registry which wraps an automatically-created new Micrometer + * {@link io.micrometer.core.instrument.composite.CompositeMeterRegistry} with a Prometheus meter registry + * automatically added, using the specified clock. + * + * @param clock default clock to associate with the new meter registry + * @param metricsConfig metrics config + * @return new wrapper around a new Micrometer composite meter registry + */ + static MMeterRegistry create(Clock clock, + MetricsConfig metricsConfig) { + CompositeMeterRegistry delegate = new CompositeMeterRegistry(ClockWrapper.create(clock)); + // The specified clock is already a Helidon one so pass it directly; no need to wrap it. + return create(ensurePrometheusRegistryIsPresent(delegate, metricsConfig), + clock, + metricsConfig); + } + + private static MMeterRegistry create(MeterRegistry delegate, + Clock neutralClock, + MetricsConfig metricsConfig) { + return new MMeterRegistry(delegate, neutralClock, metricsConfig); + } + + private static MeterRegistry ensurePrometheusRegistryIsPresent(MeterRegistry meterRegistry, + MetricsConfig metricsConfig) { + if (meterRegistry instanceof CompositeMeterRegistry compositeMeterRegistry) { + if (compositeMeterRegistry.getRegistries() + .stream() + .noneMatch(r -> r instanceof PrometheusMeterRegistry)) { + compositeMeterRegistry.add( + new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(null))); + } + } + return meterRegistry; } private final MeterRegistry delegate; + /** + * Helidon API clock to be returned by the {@link #clock} method. + */ + private final Clock clock; + private final ConcurrentHashMap meters = new ConcurrentHashMap<>(); private MMeterRegistry(MeterRegistry delegate, + Clock clock, MetricsConfig metricsConfig) { this.delegate = delegate; + this.clock = clock; delegate.config() .onMeterAdded(this::recordAdd) .onMeterRemoved(this::recordRemove); @@ -79,6 +150,11 @@ public Collection meters(Predicate> HM getOrCreate(HB builder) { @@ -204,4 +280,30 @@ private void recordRemove(Meter removedMeter) { LOGGER.log(System.Logger.Level.WARNING, "No matching neutral meter for implementation meter " + removedMeter); } } + + /** + * Micrometer-friendly wrapper around a Helidon clock. + */ + private static class ClockWrapper implements io.micrometer.core.instrument.Clock { + + static ClockWrapper create(Clock clock) { + return new ClockWrapper(clock); + } + + private final Clock neutralClock; + + private ClockWrapper(Clock neutralClock) { + this.neutralClock = neutralClock; + } + + @Override + public long wallTime() { + return neutralClock.wallTime(); + } + + @Override + public long monotonicTime() { + return neutralClock.monotonicTime(); + } + } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index 8e70248b2de..ba1656b82bb 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -22,7 +22,7 @@ import io.helidon.metrics.api.HistogramSnapshot; -import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Timer; class MTimer extends MMeter implements io.helidon.metrics.api.Timer { @@ -40,19 +40,27 @@ static Sample start() { } static Sample start(io.helidon.metrics.api.MeterRegistry meterRegistry) { - if (meterRegistry instanceof MeterRegistry mMeterRegistry) { - return Sample.create(Timer.start(mMeterRegistry)); + if (meterRegistry instanceof MMeterRegistry mMeterRegistry) { + return Sample.create(Timer.start(mMeterRegistry.delegate())); } throw new IllegalArgumentException("Expected meter registry type " + MMeterRegistry.class.getName() + " but was " + meterRegistry.getClass().getName()); } static Sample start(io.helidon.metrics.api.Clock clock) { - if (clock instanceof MClock mClock) { - return Sample.create(Timer.start(mClock.delegate())); - } - throw new IllegalArgumentException("Expected clock type " + MClock.class.getName() - + " but was " + clock.getClass().getName()); + // This is a relatively infrequently-used method, so it is not overly costly + // to create a new instance of Micrometer's Clock each invocation. + return Sample.create(Timer.start(new Clock() { + @Override + public long wallTime() { + return clock.wallTime(); + } + + @Override + public long monotonicTime() { + return clock.monotonicTime(); + } + })); } static class Sample implements io.helidon.metrics.api.Timer.Sample { @@ -162,32 +170,14 @@ private Builder(String name) { } @Override - public Builder publishPercentiles(double... percentiles) { + public Builder percentiles(double... percentiles) { delegate().publishPercentiles(percentiles); return identity(); } @Override - public Builder percentilePrecision(Integer digitsOfPrecision) { - delegate().percentilePrecision(digitsOfPrecision); - return identity(); - } - - @Override - public Builder publishPercentileHistogram() { - delegate().publishPercentileHistogram(); - return identity(); - } - - @Override - public Builder publishPercentileHistogram(Boolean enabled) { - delegate().publishPercentileHistogram(enabled); - return identity(); - } - - @Override - public Builder serviceLevelObjectives(Duration... slos) { - delegate().serviceLevelObjectives(slos); + public Builder buckets(Duration... buckets) { + delegate().serviceLevelObjectives(buckets); return identity(); } @@ -202,17 +192,5 @@ public Builder maximumExpectedValue(Duration max) { delegate().maximumExpectedValue(max); return identity(); } - - @Override - public Builder distributionStatisticExpiry(Duration expiry) { - delegate().distributionStatisticExpiry(expiry); - return identity(); - } - - @Override - public Builder distributionStatisticBufferLength(Integer bufferLength) { - delegate().distributionStatisticBufferLength(bufferLength); - return identity(); - } } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index d7d46eb87b9..98802f24295 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -64,6 +64,11 @@ public MeterRegistry createMeterRegistry(MetricsConfig metricsConfig) { return MMeterRegistry.create(metricsConfig); } + @Override + public MeterRegistry createMeterRegistry(Clock clock, MetricsConfig metricsConfig) { + return MMeterRegistry.create(clock, metricsConfig); + } + @Override public MeterRegistry globalRegistry() { return globalMeterRegistry.get(); diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java new file mode 100644 index 00000000000..32803f75803 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestDistributionSummary.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.List; + +import io.helidon.metrics.api.DistributionSummary; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class TestDistributionSummary { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testUnwrap() { + DistributionSummary summary = meterRegistry.getOrCreate(DistributionSummary.builder("a")); + List.of(1D, 3D, 5D) + .forEach(summary::record); + io.micrometer.core.instrument.DistributionSummary mSummary = + summary.unwrap(io.micrometer.core.instrument.DistributionSummary.class); + + mSummary.record(7D); + + assertThat("Mean", summary.mean(), is(4D)); + assertThat("Min", summary.max(), is(7D)); + assertThat("Count", summary.count(), is(4L)); + assertThat("Total", summary.totalAmount(), is(16D)); + + assertThat("Mean (Micrometer)", mSummary.mean(), is(4D)); + assertThat("Min (Micrometer)", mSummary.max(), is(7D)); + assertThat("Count (Micrometer)", mSummary.count(), is(4L)); + assertThat("Total (Micrometer)", mSummary.totalAmount(), is(16D)); + } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestTimer.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestTimer.java new file mode 100644 index 00000000000..3a64438d865 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestTimer.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import io.helidon.metrics.api.Gauge; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Timer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; + +class TestTimer { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + @Test + void testUnwrap() { + long initialValue = 0L; + long incrA = 2L; + long incrB = 7L; + Timer t = meterRegistry.getOrCreate(Timer.builder("a")); + + io.micrometer.core.instrument.Timer mTimer = t.unwrap(io.micrometer.core.instrument.Timer.class); + assertThat("Initial value", mTimer.count(), is(initialValue)); + + t.record(incrA, TimeUnit.MILLISECONDS); + mTimer.record(incrB, TimeUnit.MILLISECONDS); + + assertThat("Neutral count after updates", t.count(), is(2L)); + assertThat("Micrometer count after updates", mTimer.count(), is(2L)); + + double fromMicrometer = mTimer.totalTime(TimeUnit.MILLISECONDS); + assertThat("Updated Micrometer value", + fromMicrometer, + greaterThanOrEqualTo((double) incrA + incrB)); + assertThat("Updated Micrometer value", + t.totalTime(TimeUnit.MILLISECONDS), + is(fromMicrometer)); + } +} diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestTimer.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestTimer.java new file mode 100644 index 00000000000..02a293ab98e --- /dev/null +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestTimer.java @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.testing; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import io.helidon.metrics.api.Clock; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; +import io.helidon.metrics.api.Timer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; + +class TestTimer { + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create()); + } + + + @Test + void testSimpleRecord() { + Timer t = meterRegistry.getOrCreate(Timer.builder("a")); + + long initialValue = 0L; + + assertThat("Initial value", + t.count(), + is(0L)); + assertThat("Initial value", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue)); + + long update = 12L; + t.record(update, TimeUnit.MILLISECONDS); + assertThat("Updated value", + t.count(), + is(1L)); + assertThat("Updated value", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue + update)); + + initialValue += update; + update = 7L; + t.record(Duration.ofMillis(update)); + assertThat("Second updated value", + t.count(), + is(2L)); + assertThat("Second updated value", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue + update)); + } + + @Test + void testCallable() throws Exception { + Timer t = meterRegistry.getOrCreate(Timer.builder("b")); + + long initialValue = 0L; + long update = 12L; + + t.record((Callable) () -> { + TimeUnit.MILLISECONDS.sleep(update); + return null; + }); + + assertThat("After update", + t.count(), + is(1L)); + assertThat("After update", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue + update)); + } + + @Test + void testSupplier() { + Timer t = meterRegistry.getOrCreate(Timer.builder("c")); + long initialValue = 0L; + long update = 8L; + + t.record((Supplier) () -> { + try { + TimeUnit.MILLISECONDS.sleep(update); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + + assertThat("After update", + t.count(), + is(1L)); + assertThat("After update", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue + update)); + } + + @Test + void testWrapCallable() throws Exception { + Timer t = meterRegistry.getOrCreate(Timer.builder("d")); + long initialValue = 0L; + long update = 18L; + + Callable c = t.wrap((Callable) () -> { + try { + TimeUnit.MILLISECONDS.sleep(update); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return null; + }); + + assertThat("Before running", + t.count(), + is(0L)); + assertThat("Before running", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue)); + + c.call(); + + assertThat("After running", + t.count(), + is(1L)); + assertThat("After running", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) initialValue + update)); + } + + @Test + void testSample() throws InterruptedException { + Timer t = meterRegistry.getOrCreate(Timer.builder("e")); + long initialValue = 0L; + long update = 18L; + + Timer.Sample sample = Timer.start(); + + long waitTime = 110L; + TimeUnit.MILLISECONDS.sleep(waitTime); + + sample.stop(t); + + assertThat("After sample stop", + t.count(), + is(1L)); + assertThat("After sample stop", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) waitTime)); + + + } + + @Test + void testSampleWithExplicitClock() { + Timer t = meterRegistry.getOrCreate(Timer.builder("f")); + AdjustableClock clock = new AdjustableClock(); + + Timer.Sample sample = Timer.start(clock); + + long waitTime = 55L; + clock.advance(waitTime); + + sample.stop(t); + + assertThat("After sample stop", + t.count(), + is(1L)); + assertThat("After sample stop", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) waitTime)); + } + + @Test + void testSampleWithImplicitClock() { + AdjustableClock clock = new AdjustableClock(); + + MeterRegistry registry = MetricsFactory.getInstance() + .createMeterRegistry(clock, MetricsConfig.builder().build()); + + Timer t = registry.getOrCreate(Timer.builder("g")); + + Timer.Sample sample = Timer.start(registry); + + long waitTime = 35L; + clock.advance(waitTime); + + sample.stop(t); + + assertThat("After sample stop", + t.count(), + is(1L)); + assertThat("After sample stop", + t.totalTime(TimeUnit.MILLISECONDS), + greaterThanOrEqualTo((double) waitTime)); + + } + + private static class AdjustableClock implements Clock { + + private long wallTime; + private long monotonicTime; + + private AdjustableClock() { + this.wallTime = System.currentTimeMillis(); + this.monotonicTime = System.nanoTime(); + } + + @Override + public long wallTime() { + return wallTime; + } + + @Override + public long monotonicTime() { + return monotonicTime; + } + + private void advance(long ms) { + wallTime += ms; + monotonicTime += ms * 1000 * 1000; + } + } +} From 2bf97c731d4754a04bba4ed106ec6eb2c84dba64 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 02:40:56 -0500 Subject: [PATCH 32/40] Some clean-up --- .../main/java/io/helidon/metrics/api/Meter.java | 3 ++- .../metrics/api/MetricsConfigBlueprint.java | 2 +- .../helidon/metrics/api/MetricsConfigSupport.java | 2 +- .../io/helidon/metrics/api/MetricsFactory.java | 15 +++++++++------ .../metrics/api/MetricsFactoryManager.java | 2 +- .../java/io/helidon/metrics/api/NoOpMeter.java | 9 --------- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index 547a451bfdc..f24b4f46b19 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -26,7 +26,7 @@ public interface Meter extends Wrapped { * @param type of the builder * @param type of the meter the builder creates */ - interface Builder, M extends Meter> /* extends BuilderAdapter */ { + interface Builder, M extends Meter> { /** * Returns the type-correct "this". @@ -47,6 +47,7 @@ default B identity() { /** * Sets the description. + * * @param description meter description * @return updated builder */ diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index af57ea38b56..be5865388be 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -30,7 +30,7 @@ import io.helidon.inject.configdriven.api.ConfigBean; /** - * Config bean for {@link io.helidon.metrics.api.MetricsConfig}. + * Blueprint for {@link io.helidon.metrics.api.MetricsConfig}. */ @ConfigBean() @Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java index d5b6759b2c1..7946db17d8a 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.java @@ -41,7 +41,7 @@ static Optional lookupConfig(MetricsConfig metricsConfig, String key) { // - capture reluctant match of anything // - non-capturing match of an unescaped = // - capture the rest. - static final Pattern TAG_ASSIGNMENT_PATTERN = Pattern.compile("(.*?)(?:(? *

- * Also, various static methods create or return previously-created instances. + * Also, various static methods create new instances or return previously-created ones. *

*

* Note that this is not intended to be the interface which developers use to work with Helidon metrics. - * Instead they should use the {@link io.helidon.metrics.api.Metrics} interface and its static convenience methods - * or use {@link io.helidon.metrics.api.Metrics#globalRegistry()} and use the returned - * {@link io.helidon.metrics.api.MeterRegistry} directly. - *

+ * Instead use + *
    + *
  • the {@link io.helidon.metrics.api.Metrics} interface and its static convenience methods,
  • + *
  • the static methods on the various interfaces in the API, or
  • + *
  • {@link io.helidon.metrics.api.Metrics#globalRegistry()} and use the returned + * {@link io.helidon.metrics.api.MeterRegistry} directly
  • + *
*

* Rather, implementations of Helidon metrics implement this interface and various internal parts of Helidon metrics, * notably the static methods on {@link io.helidon.metrics.api.Metrics}, delegate to the highest-weight @@ -44,7 +47,7 @@ public interface MetricsFactory { /** - * Returns the highest-weight implementation of the factory available at runtime. + * Returns an implementation which has the highest weight of the factories available at runtime. * * @return highest-weight metrics factory */ diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java index f4eb48def79..f87d5563144 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java @@ -23,7 +23,7 @@ import io.helidon.metrics.spi.MetricsFactoryProvider; /** - * Locates and makes available the highest-weight implementation of {@link io.helidon.metrics.spi.MetricsProvider}, + * Locates and makes available a highest-weight implementation of {@link io.helidon.metrics.spi.MetricsFactoryProvider}, * using a default no-op implementation if no other is available. */ class MetricsFactoryManager { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 423aac96003..77d718a99cf 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -18,7 +18,6 @@ import java.io.PrintStream; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -46,10 +45,6 @@ static Id create(String name, Iterable tags) { return new Id(name, tags); } - static Id create(String name, Tag... tags) { - return new Id(name, Arrays.asList(tags)); - } - private final String name; private final List tags = new ArrayList<>(); // must be ordered by tag name for consistency @@ -69,10 +64,6 @@ public List tags() { return tags.stream().toList(); } - Iterable tagsAsIterable() { - return tags; - } - String tag(String key) { return tags.stream() .filter(t -> t.key().equals(key)) From e8643c0f2aa8d4c352d7f66e7ab415c97681bd3c Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 10:25:11 -0500 Subject: [PATCH 33/40] Clean up bom --- bom/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 0a361c68f99..4aadf6b13fd 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -394,11 +394,6 @@ helidon-metrics-trace-exemplar ${helidon.version} - - io.helidon.tests.integration.metrics - helidon-tests-integration-metrics-common - ${helidon.version} - From 1d12c74f79172b65739d3944db52b6b986479107 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 10:28:49 -0500 Subject: [PATCH 34/40] Fix line length --- .../api/src/main/java/io/helidon/metrics/api/NoOpMeter.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 77d718a99cf..31e76c76568 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -546,7 +546,8 @@ public double max(TimeUnit unit) { } } - static class DistributionStatisticsConfig implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapped { + static class DistributionStatisticsConfig + implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapped { static Builder builder() { return new Builder(); @@ -556,7 +557,7 @@ static class Builder implements io.helidon.metrics.api.DistributionStatisticsCon @Override public io.helidon.metrics.api.DistributionStatisticsConfig build() { - return new NoOpMeter.DistributionStatisticsConfig(this); + return new DistributionStatisticsConfig(this); } @Override From 1480ce121e024fa31e549c6be9dddfaf3f713f3d Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 12:31:41 -0500 Subject: [PATCH 35/40] Rename Wrapped to Wrapper --- .../src/main/java/io/helidon/metrics/api/Clock.java | 2 +- .../java/io/helidon/metrics/api/CountAtBucket.java | 2 +- .../metrics/api/DistributionStatisticsConfig.java | 4 ++-- .../java/io/helidon/metrics/api/HistogramSnapshot.java | 2 +- .../src/main/java/io/helidon/metrics/api/Meter.java | 2 +- .../java/io/helidon/metrics/api/MeterRegistry.java | 2 +- .../main/java/io/helidon/metrics/api/NoOpMeter.java | 8 ++++---- .../java/io/helidon/metrics/api/NoOpMeterRegistry.java | 2 +- .../src/main/java/io/helidon/metrics/api/NoOpTag.java | 2 +- .../metrics/api/{NoOpWrapped.java => NoOpWrapper.java} | 2 +- .../api/src/main/java/io/helidon/metrics/api/Tag.java | 2 +- .../java/io/helidon/metrics/api/ValueAtPercentile.java | 2 +- .../helidon/metrics/api/{Wrapped.java => Wrapper.java} | 10 +++++----- 13 files changed, 21 insertions(+), 21 deletions(-) rename metrics/api/src/main/java/io/helidon/metrics/api/{NoOpWrapped.java => NoOpWrapper.java} (94%) rename metrics/api/src/main/java/io/helidon/metrics/api/{Wrapped.java => Wrapper.java} (73%) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java index 3b3f746d06c..12115a5bd89 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java @@ -18,7 +18,7 @@ /** * Reports absolute time (and, therefore, is also useful in computing elapsed times). */ -public interface Clock extends Wrapped { +public interface Clock extends Wrapper { /** * Returns the system clock for the Helidon metrics implementation. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java index 28a3b5380ac..6e43dbad908 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java @@ -24,7 +24,7 @@ * That is, an observation occupies a bucket if its value is less than or equal to the bucket's boundary value. *

*/ -public interface CountAtBucket extends Wrapped { +public interface CountAtBucket extends Wrapper { /** * Returns the bucket boundary. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index 082d217dd8f..f03c08998bc 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -22,7 +22,7 @@ * (for example, timers and distribution summaries). * */ -public interface DistributionStatisticsConfig extends Wrapped { +public interface DistributionStatisticsConfig extends Wrapper { /** * Creates a builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. @@ -64,7 +64,7 @@ static Builder builder() { /** * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance. */ - interface Builder extends Wrapped, io.helidon.common.Builder { + interface Builder extends Wrapper, io.helidon.common.Builder { /** * Sets the minimum value that the meter is expected to observe. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java index c3dfa29f23b..43528996e56 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -21,7 +21,7 @@ /** * Snapshot in time of a histogram. */ -public interface HistogramSnapshot extends Wrapped { +public interface HistogramSnapshot extends Wrapper { /** * Returns an "empty" snapshot which has summary values but no data points. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java index f24b4f46b19..214ff56a9c5 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java @@ -18,7 +18,7 @@ /** * Common behavior of all meters. */ -public interface Meter extends Wrapped { +public interface Meter extends Wrapper { /** * Common behavior of specific meter builders. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 95bdc5182a0..0ceea8d5e63 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -23,7 +23,7 @@ /** * Manages the look-up and registration of meters. */ -public interface MeterRegistry extends Wrapped { +public interface MeterRegistry extends Wrapper { /** * Returns all previously-registered meters. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 31e76c76568..5e93bfec357 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -33,7 +33,7 @@ /** * No-op implementation of the Helidon {@link io.helidon.metrics.api.Meter} interface. */ -class NoOpMeter implements Meter, NoOpWrapped { +class NoOpMeter implements Meter, NoOpWrapper { private final Id id; private final String unit; @@ -321,7 +321,7 @@ public io.helidon.metrics.api.HistogramSnapshot snapshot() { } } - static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot, NoOpWrapped { + static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot, NoOpWrapper { private final long count; private final double total; @@ -547,13 +547,13 @@ public double max(TimeUnit unit) { } static class DistributionStatisticsConfig - implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapped { + implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapper { static Builder builder() { return new Builder(); } - static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder, NoOpWrapped { + static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder, NoOpWrapper { @Override public io.helidon.metrics.api.DistributionStatisticsConfig build() { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index a59b8a9750c..80a34f72858 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -28,7 +28,7 @@ * store meters or their IDs, in line with the documented behavior of disabled metrics. *

*/ -class NoOpMeterRegistry implements MeterRegistry, NoOpWrapped { +class NoOpMeterRegistry implements MeterRegistry, NoOpWrapper { @Override public List meters() { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java index 8d3e3364de7..96d1c6523ca 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java @@ -18,7 +18,7 @@ import java.util.Iterator; import java.util.NoSuchElementException; -record NoOpTag(String key, String value) implements Tag, NoOpWrapped { +record NoOpTag(String key, String value) implements Tag, NoOpWrapper { static Tag create(String key, String value) { return new NoOpTag(key, value); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.java similarity index 94% rename from metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java rename to metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.java index 6ac9c49ae60..1dd1b6b4ebb 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapped.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.java @@ -15,7 +15,7 @@ */ package io.helidon.metrics.api; -interface NoOpWrapped extends Wrapped { +interface NoOpWrapper extends Wrapper { @Override default R unwrap(Class c) { diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java index 96bc14f5ad7..b0e28e3e626 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Tag.java @@ -18,7 +18,7 @@ /** * Behavior of a tag for further identifying meters. */ -public interface Tag extends Wrapped { +public interface Tag extends Wrapper { /** * Returns the tag's key. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java b/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java index 7e2b3881142..c981d4dcbbe 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/ValueAtPercentile.java @@ -20,7 +20,7 @@ /** * Percentile and value at that percentile within a distribution. */ -public interface ValueAtPercentile extends Wrapped { +public interface ValueAtPercentile extends Wrapper { /** * Returns the percentile. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java similarity index 73% rename from metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java rename to metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java index 5f4ed610f8d..9b35c7f8c9a 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapped.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java @@ -16,15 +16,15 @@ package io.helidon.metrics.api; /** - * Behavior of a type that wraps a related type. + * Behavior of a type that wraps a related type, typically through delegation. */ -public interface Wrapped { +public interface Wrapper { /** - * Unwraps the wrapped item as the specified type. + * Unwraps the delegate as the specified type. * - * @param c {@link Class} to which to cast this object - * @return this object cast as the requested type + * @param c {@link Class} to which to cast the delegate + * @return the delegate cast as the requested type * @param type to cast to */ R unwrap(Class c); From 4ea2a6bc6a6fc87354419ed1c2fc7cf05bf740aa Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 12:54:27 -0500 Subject: [PATCH 36/40] Review comments; some other clean-up --- .../api/{CountAtBucket.java => Bucket.java} | 12 +++++----- .../api/DistributionStatisticsConfig.java | 12 +++++----- .../metrics/api/HistogramSnapshot.java | 4 ++-- .../io/helidon/metrics/api/NoOpMeter.java | 2 +- .../java/io/helidon/metrics/api/Timer.java | 4 ++-- .../java/io/helidon/metrics/api/Wrapper.java | 1 + .../{MCountAtBucket.java => MBucket.java} | 22 ++++++++++--------- .../micrometer/MHistogramSnapshot.java | 8 ++++--- .../micrometer/MicrometerMetricsFactory.java | 14 +++--------- .../testing/TestDistributionSummary.java | 12 +++++----- 10 files changed, 44 insertions(+), 47 deletions(-) rename metrics/api/src/main/java/io/helidon/metrics/api/{CountAtBucket.java => Bucket.java} (78%) rename metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/{MCountAtBucket.java => MBucket.java} (74%) diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java b/metrics/api/src/main/java/io/helidon/metrics/api/Bucket.java similarity index 78% rename from metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java rename to metrics/api/src/main/java/io/helidon/metrics/api/Bucket.java index 6e43dbad908..465f9948d93 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/CountAtBucket.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Bucket.java @@ -18,29 +18,29 @@ import java.util.concurrent.TimeUnit; /** - * Representation of a histogram bucket, including the bucket boundary value and the count of observations in that bucket. + * Representation of a histogram bucket, including the boundary value and the count of observations in that bucket. *

- * The bucket boundary value is an upper bound on the observation values that can occupy the bucket. + * The boundary value is an upper bound on the observation values that can occupy the bucket. * That is, an observation occupies a bucket if its value is less than or equal to the bucket's boundary value. *

*/ -public interface CountAtBucket extends Wrapper { +public interface Bucket extends Wrapper { /** * Returns the bucket boundary. * * @return bucket boundary value */ - double bucket(); + double boundary(); /** - * Returns the bucket boundary interpreted as a time in nanoseconds andexpressed in the specified + * Returns the bucket boundary interpreted as a time in nanoseconds and expressed in the specified * {@link java.util.concurrent.TimeUnit}. * * @param unit time unit in which to express the bucket boundary * @return bucket boundary value */ - double bucket(TimeUnit unit); + double boundary(TimeUnit unit); /** * Returns the number of observations in the bucket. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java index f03c08998bc..4d64dfc9055 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.java @@ -55,9 +55,9 @@ static Builder builder() { Optional maximumExpectedValue(); /** - * Returns the configured bucket boundaries. + * Returns the configured boundary boundaries. * - * @return the bucket boundaries + * @return the boundary boundaries */ Optional> buckets(); @@ -111,17 +111,17 @@ interface Builder extends Wrapper, io.helidon.common.Builder percentiles); /** - * Sets the bucket boundaries. + * Sets the boundary boundaries. * - * @param buckets bucket boundaries + * @param buckets boundary boundaries * @return updated builder */ Builder buckets(double... buckets); /** - * Sets the bucket boundaries. + * Sets the boundary boundaries. * - * @param buckets bucket boundaries + * @param buckets boundary boundaries * @return updated builder */ Builder buckets(Iterable buckets); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java index 43528996e56..377001f6871 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/HistogramSnapshot.java @@ -91,9 +91,9 @@ static HistogramSnapshot empty(long count, double total, double max) { /** * Returns information about each of the configured buckets for the histogram. * - * @return pairs of bucket value and count of observations in that bucket + * @return pairs of boundary value and count of observations in that boundary */ - Iterable histogramCounts(); + Iterable histogramCounts(); /** * Dumps a summary of the snapshot to the specified {@link java.io.PrintStream} using the indicated scaling factor for diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java index 5e93bfec357..bd460db28ff 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java @@ -373,7 +373,7 @@ public Iterable percentileValues() { } @Override - public Iterable histogramCounts() { + public Iterable histogramCounts() { return Set.of(); } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java index 3cc4eecf29e..4170f7db0af 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java @@ -200,9 +200,9 @@ interface Builder extends Meter.Builder { Builder percentiles(double... percentiles); /** - * Sets the bucket boundaries. + * Sets the boundary boundaries. * - * @param buckets bucket boundaries + * @param buckets boundary boundaries * @return updated builder */ Builder buckets(Duration... buckets); diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java index 9b35c7f8c9a..92310adbf4d 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java @@ -26,6 +26,7 @@ public interface Wrapper { * @param c {@link Class} to which to cast the delegate * @return the delegate cast as the requested type * @param type to cast to + * @throws java.lang.ClassCastException if the delegate is not compatible with the requested type */ R unwrap(Class c); } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java similarity index 74% rename from metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java rename to metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java index d7a77916de5..8218520c1f6 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCountAtBucket.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java @@ -18,27 +18,29 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; +import io.helidon.metrics.api.Bucket; + import io.micrometer.core.instrument.distribution.CountAtBucket; -class MCountAtBucket implements io.helidon.metrics.api.CountAtBucket { +class MBucket implements Bucket { - static MCountAtBucket create(CountAtBucket delegate) { - return new MCountAtBucket(delegate); + static MBucket create(CountAtBucket delegate) { + return new MBucket(delegate); } private final CountAtBucket delegate; - private MCountAtBucket(CountAtBucket delegate) { + private MBucket(CountAtBucket delegate) { this.delegate = delegate; } @Override - public double bucket() { + public double boundary() { return delegate.bucket(); } @Override - public double bucket(TimeUnit unit) { + public double boundary(TimeUnit unit) { return delegate.bucket(unit); } @@ -58,11 +60,11 @@ public boolean equals(Object o) { return true; } // Simplifies the use of test implementations in unit tests if equals does not insist that the other object - // also be a MCountAtBucket but merely implements CountAtBucket. - if (!(o instanceof io.helidon.metrics.api.CountAtBucket that)) { + // also be a MBucket but merely implements Bucket. + if (!(o instanceof Bucket that)) { return false; } - return Objects.equals(delegate.bucket(), that.bucket()) + return Objects.equals(delegate.bucket(), that.boundary()) && Objects.equals((long) delegate.count(), that.count()); } @@ -73,6 +75,6 @@ public int hashCode() { @Override public String toString() { - return String.format("MCountAtBucket[bucket=%f,count=%d]", bucket(), count()); + return String.format("MBucket[boundary=%f,count=%d]", boundary(), count()); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java index 019c7f95402..8dea154364e 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java @@ -20,6 +20,8 @@ import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; +import io.helidon.metrics.api.Bucket; + import io.micrometer.core.instrument.distribution.CountAtBucket; import io.micrometer.core.instrument.distribution.HistogramSnapshot; import io.micrometer.core.instrument.distribution.ValueAtPercentile; @@ -89,7 +91,7 @@ public io.helidon.metrics.api.ValueAtPercentile next() { } @Override - public Iterable histogramCounts() { + public Iterable histogramCounts() { return () -> new Iterator<>() { private final CountAtBucket[] counts = delegate.histogramCounts(); @@ -101,11 +103,11 @@ public boolean hasNext() { } @Override - public io.helidon.metrics.api.CountAtBucket next() { + public Bucket next() { if (!hasNext()) { throw new NoSuchElementException(); } - return MCountAtBucket.create(counts[slot++]); + return MBucket.create(counts[slot++]); } }; } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index 98802f24295..b73e7c8c7ce 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -43,16 +43,9 @@ static MicrometerMetricsFactory create(MetricsConfig metricsConfig) { return new MicrometerMetricsFactory(metricsConfig); } - private final io.micrometer.core.instrument.MeterRegistry micrometerGlobalRegistry; - - private LazyValue globalMeterRegistry; - - private final MetricsConfig metricsConfig; + private final LazyValue globalMeterRegistry; private MicrometerMetricsFactory(MetricsConfig metricsConfig) { - micrometerGlobalRegistry = Metrics.globalRegistry; - this.metricsConfig = metricsConfig; - globalMeterRegistry = LazyValue.create(() -> { ensurePrometheusRegistry(Metrics.globalRegistry, metricsConfig); return MMeterRegistry.create(Metrics.globalRegistry, metricsConfig); @@ -76,11 +69,10 @@ public MeterRegistry globalRegistry() { private static void ensurePrometheusRegistry(CompositeMeterRegistry compositeMeterRegistry, MetricsConfig metricsConfig) { - boolean prometheusRegistryPresent = compositeMeterRegistry + if (compositeMeterRegistry .getRegistries() .stream() - .anyMatch(mr -> mr instanceof PrometheusMeterRegistry); - if (!prometheusRegistryPresent) { + .noneMatch(mr -> mr instanceof PrometheusMeterRegistry)) { compositeMeterRegistry.add(new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(null))); } } diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java index d2329d54bb5..4569c8088ee 100644 --- a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java +++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import io.helidon.metrics.api.CountAtBucket; +import io.helidon.metrics.api.Bucket; import io.helidon.metrics.api.DistributionStatisticsConfig; import io.helidon.metrics.api.DistributionSummary; import io.helidon.metrics.api.HistogramSnapshot; @@ -98,7 +98,7 @@ void testBuckets() { HistogramSnapshot snapshot = summary.snapshot(); - List cabs = Util.list(snapshot.histogramCounts()); + List cabs = Util.list(snapshot.histogramCounts()); assertThat("Counts at buckets", cabs, @@ -131,15 +131,15 @@ public String toString() { } } - private record Cab(double bucket, long count) implements CountAtBucket { + private record Cab(double boundary, long count) implements Bucket { private static Cab create(double bucket, long count) { return new Cab(bucket, count); } @Override - public double bucket(TimeUnit unit) { - return unit.convert((long) bucket, TimeUnit.NANOSECONDS); + public double boundary(TimeUnit unit) { + return unit.convert((long) boundary, TimeUnit.NANOSECONDS); } @Override @@ -149,7 +149,7 @@ public R unwrap(Class c) { @Override public String toString() { - return String.format("Vap[bucket=%f,count=%d]", bucket, count); + return String.format("Vap[boundary=%f,count=%d]", boundary, count); } } } From d5ee44a62cfdf6cb9ff11f138c7bbfa95a6768a2 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 13:25:09 -0500 Subject: [PATCH 37/40] adopt latest Micrometer release 1.11.3 --- dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 2c62e745d86..1d4cb9e8827 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -98,7 +98,7 @@ 1.4.0 2.6.2 2.10 - 1.11.1 + 1.11.3 1.11.1 3.4.3 3.3.0 From 4beba7053140be497927f19cd0614e124a1647ad Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Tue, 15 Aug 2023 17:25:03 -0500 Subject: [PATCH 38/40] Update release of Micrometer Prometheus registry as well --- dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 1d4cb9e8827..ae41e1b5c41 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -99,7 +99,7 @@ 2.6.2 2.10 1.11.3 - 1.11.1 + 1.11.3 3.4.3 3.3.0 4.4.0 From f5aa39c15099f6a34d957ccdf99a7c3f4c86fd84 Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 16 Aug 2023 17:26:43 -0500 Subject: [PATCH 39/40] Remove unneeded (and moved) test-time dep. --- metrics/providers/micrometer/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml index fde4306eb87..b6c11edae74 100644 --- a/metrics/providers/micrometer/pom.xml +++ b/metrics/providers/micrometer/pom.xml @@ -53,11 +53,6 @@ hamcrest-all test - - io.helidon.nima.testing.junit5 - helidon-nima-testing-junit5-webserver - test - From 60029a4c5617a5f9568eb2d7b3a1369648e63c5b Mon Sep 17 00:00:00 2001 From: "tim.quinn@oracle.com" Date: Wed, 16 Aug 2023 18:37:04 -0500 Subject: [PATCH 40/40] Revise metrics feature with new metrics implementation; add JSON formatting back; reorg formatting --- .../io/helidon/metrics/api/MeterRegistry.java | 21 +- .../metrics/api/MeterRegistryFormatter.java | 38 ++ .../java/io/helidon/metrics/api/Metrics.java | 2 +- .../metrics/api/MetricsConfigBlueprint.java | 18 + .../helidon/metrics/api/MetricsFactory.java | 17 +- .../metrics/api/MetricsFactoryManager.java | 109 +++- .../metrics/api/NoOpMeterRegistry.java | 10 + .../spi/MeterRegistryFormatterProvider.java | 50 ++ metrics/api/src/main/java/module-info.java | 4 + .../MicrometerPrometheusFormatter.java | 1 + .../micrometer/MDistributionSummary.java | 3 + .../metrics/micrometer/MMeterRegistry.java | 94 +++- .../io/helidon/metrics/micrometer/MTimer.java | 3 + .../micrometer/MicrometerMetricsFactory.java | 37 ++ .../MicrometerPrometheusFormatter.java | 329 ++++++++++++ ...MicrometerPrometheusFormatterProvider.java | 48 ++ .../micrometer/src/main/java/module-info.java | 4 + .../micrometer/TestPrometheusFormatting.java | 178 +++++++ .../metrics/micrometer/TestScopes.java | 64 +++ webserver/observe/metrics/pom.xml | 28 + .../observe/metrics/JsonFormatter.java | 502 ++++++++++++++++++ .../JsonMeterRegistryFormatterProvider.java | 46 ++ .../KeyPerformanceIndicatorMetricsImpls.java | 28 +- .../observe/metrics/MetricsFeature.java | 226 +++++--- .../metrics/src/main/java/module-info.java | 8 + .../observe/metrics/TestJsonFormatting.java | 114 ++++ 26 files changed, 1860 insertions(+), 122 deletions(-) create mode 100644 metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistryFormatter.java create mode 100644 metrics/api/src/main/java/io/helidon/metrics/spi/MeterRegistryFormatterProvider.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatter.java create mode 100644 metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatterProvider.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestPrometheusFormatting.java create mode 100644 metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestScopes.java create mode 100644 webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonFormatter.java create mode 100644 webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonMeterRegistryFormatterProvider.java create mode 100644 webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestJsonFormatting.java diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java index 0ceea8d5e63..906fb718ae0 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java @@ -30,7 +30,7 @@ public interface MeterRegistry extends Wrapper { * * @return registered meters */ - List meters(); + List meters(); /** * Returns previously-registered meters which match the specified {@link java.util.function.Predicate}. @@ -38,7 +38,24 @@ public interface MeterRegistry extends Wrapper { * @param filter the predicate with which to evaluate each {@link io.helidon.metrics.api.Meter} * @return meters which match the predicate */ - Collection meters(Predicate filter); + Collection meters(Predicate filter); + + + /** + * Returns the scopes (if any) as represented by tags on meter IDs. + * + * @return scopes across all registered meters + */ + Iterable scopes(); + + /** + * Returns whether the specified meter is enabled or not, based on whether the meter registry as a whole is enabled and also + * whether the config settings for filtering include and exclude indicate the specific meter is enabled. + * + * @param meterId meter ID to check + * @return true if the meter (and its meter registry) are enabled; false otherwise + */ + boolean isMeterEnabled(Meter.Id meterId); /** * Returns the default {@link io.helidon.metrics.api.Clock} in use by the registry. diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistryFormatter.java b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistryFormatter.java new file mode 100644 index 00000000000..00ae8905f95 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistryFormatter.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.api; + +import java.util.Optional; + +/** + * Formatter of a {@link io.helidon.metrics.api.MeterRegistry} according to a given media type. + */ +public interface MeterRegistryFormatter { + + /** + * Formats the meter registry's data. + * + * @return formatted output + */ + Optional format(); + + /** + * Formats the meter registry's metadata. + * + * @return formatted metadata output + */ + Optional formatMetadata(); +} diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java index 55fac159a31..a1d7080284f 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.java @@ -37,7 +37,7 @@ static MeterRegistry globalRegistry() { /** * Creates a meter registry, not added to the global registry, based on - * the provide metrics config. + * the provided metrics config. * * @param metricsConfig metrics config * @return new meter registry diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java index be5865388be..49153586d88 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java @@ -53,6 +53,12 @@ interface MetricsConfigBlueprint { */ String APP_TAG_CONFIG_KEY = "app-name"; + // TODO - rely on programmatic metrics config so SE can set one default, MP another + /** + * Default name for scope tags. + */ + String DEFAULT_SCOPE_TAG_NAME = "m-scope"; + /** * Whether metrics functionality is enabled. * @@ -92,6 +98,15 @@ interface MetricsConfigBlueprint { */ Config config(); + /** + * Tag name used for recording the scope of meters (defaults according to the active runtime). + * + * @return tag name for scope + */ + String scopeTagName(); + + + class BuilderDecorator implements Prototype.BuilderDecorator> { @Override @@ -99,6 +114,9 @@ public void decorate(MetricsConfig.BuilderBase builder) { if (builder.config().isEmpty()) { builder.config(GlobalConfig.config().get(METRICS_CONFIG_KEY)); } + if (builder.scopeTagName().isEmpty()) { + builder.scopeTagName(DEFAULT_SCOPE_TAG_NAME); + } } } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java index 8524c65a834..28ce77f5a33 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactory.java @@ -47,14 +47,27 @@ public interface MetricsFactory { /** - * Returns an implementation which has the highest weight of the factories available at runtime. + * Returns the most-recently created implementation or, if none, a new one from a highest-weight provider available at + * runtime and using the {@value MetricsConfig.Builder#METRICS_CONFIG_KEY} section from the + * {@link io.helidon.common.config.GlobalConfig}. * - * @return highest-weight metrics factory + * @return current or new metrics factory */ static MetricsFactory getInstance() { return MetricsFactoryManager.getInstance(); } + /** + * Returns a new instance from a highest-weight provider available at runtime using the provided + * {@link io.helidon.metrics.api.MetricsConfig}. + * + * @param metricsConfig metrics config + * @return new metrics factory + */ + static MetricsFactory getInstance(MetricsConfig metricsConfig) { + return MetricsFactoryManager.getInstance(metricsConfig); + } + /** * Returns the global meter registry. * diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java index f87d5563144..f103a14acd1 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java @@ -15,7 +15,10 @@ */ package io.helidon.metrics.api; +import java.util.Objects; import java.util.ServiceLoader; +import java.util.concurrent.Callable; +import java.util.concurrent.locks.ReentrantLock; import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; @@ -23,31 +26,109 @@ import io.helidon.metrics.spi.MetricsFactoryProvider; /** - * Locates and makes available a highest-weight implementation of {@link io.helidon.metrics.spi.MetricsFactoryProvider}, - * using a default no-op implementation if no other is available. + * Provides {@link io.helidon.metrics.api.spi.MetricFactory} instances using a highest-weight implementation of + * {@link io.helidon.metrics.spi.MetricsFactoryProvider}, defaulting to a no-op implementation if no other is available. + *

+ * The {@link #getInstance()} and {@link #getInstance(MetricsConfig)} methods update and use the most-recently used + * {@link io.helidon.metrics.api.MetricsConfig} (which could be derived from {@link io.helidon.common.config.GlobalConfig}) + * and the most-recently created {@link io.helidon.metrics.api.MetricsFactory}. + *

+ *

+ * The {@link #create(MetricsConfig)} method neither reads nor updates the most-recently used config and factory. + *

*/ class MetricsFactoryManager { + private static final System.Logger LOGGER = System.getLogger(MetricsFactoryManager.class.getName()); + /** * Instance of the highest-weight implementation of {@link io.helidon.metrics.spi.MetricsFactoryProvider}. */ private static final LazyValue METRICS_FACTORY_PROVIDER = - LazyValue.create(() -> HelidonServiceLoader.builder(ServiceLoader.load(MetricsFactoryProvider.class)) - .addService(NoOpMetricsFactoryProvider.create(), Double.MIN_VALUE) - .build() - .iterator() - .next()); - - private static final LazyValue METRICS_FACTORY = - LazyValue.create(() -> METRICS_FACTORY_PROVIDER.get().create( - MetricsConfig.builder() - .config(GlobalConfig.config().get(MetricsConfig.METRICS_CONFIG_KEY)) - .build())); + LazyValue.create(() -> { + MetricsFactoryProvider result = HelidonServiceLoader.builder(ServiceLoader.load(MetricsFactoryProvider.class)) + .addService(NoOpMetricsFactoryProvider.create(), Double.MIN_VALUE) + .build() + .iterator() + .next(); + LOGGER.log(System.Logger.Level.DEBUG, "Loaded metrics factory provider: {0}", + result.getClass().getName()); + return result; + }); + + /** + * The {@link io.helidon.metrics.api.MetricsFactory} most recently created via either {@link #getInstance} method. + */ + private static MetricsFactory metricsFactory; + + /** + * The {@link io.helidon.metrics.api.MetricsConfig} used to create {@link #metricsFactory}. + */ + private static MetricsConfig metricsConfig; + + private static final ReentrantLock LOCK = new ReentrantLock(); + + /** + * Creates a new {@link io.helidon.metrics.api.MetricsFactory} according to the specified + * {@link io.helidon.metrics.api.MetricsConfig}, saving the config as the current config and the new factory as the current + * factory. + * + * @param metricsConfig metrics config + * @return new metrics factory + */ + static MetricsFactory getInstance(MetricsConfig metricsConfig) { + return access(() -> { + MetricsFactoryManager.metricsConfig = metricsConfig; + MetricsFactoryManager.metricsFactory = METRICS_FACTORY_PROVIDER.get().create(metricsConfig); + return MetricsFactoryManager.metricsFactory; + }); + } + /** + * Returns the current {@link io.helidon.metrics.api.MetricsFactory}, creating one if needed using the global configuration + * and saving the {@link io.helidon.metrics.api.MetricsConfig} from the global config as the current config and the new + * factory as the current factory. + * + * @return current metrics factory + */ static MetricsFactory getInstance() { - return METRICS_FACTORY.get(); + return access(() -> metricsFactory = Objects.requireNonNullElseGet(metricsFactory, + () -> METRICS_FACTORY_PROVIDER.get() + .create(ensureMetricsConfig()))); + } + + /** + * Creates a new {@link io.helidon.metrics.api.MetricsFactory} using the specified + * {@link io.helidon.metrics.api.MetricsConfig} with no side effects: neither the config nor the new factory replace + * the current values stored in this manager. + * + * @param metricsConfig metrics config to use in creating the factory + * @return new metrics factory + */ + static MetricsFactory create(MetricsConfig metricsConfig) { + return METRICS_FACTORY_PROVIDER.get().create(metricsConfig); + } + + private static MetricsConfig ensureMetricsConfig() { + metricsConfig = Objects.requireNonNullElseGet(metricsConfig, + () -> MetricsConfig + .create(GlobalConfig.config() + .get(MetricsConfig.METRICS_CONFIG_KEY))); + return metricsConfig; + } + + private static T access(Callable c) { + LOCK.lock(); + try { + return c.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + LOCK.unlock(); + } } private MetricsFactoryManager() { } + } diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java index 80a34f72858..2316d2fc202 100644 --- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java +++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java @@ -40,6 +40,16 @@ public Collection meters(Predicate filter) { return Set.of(); } + @Override + public Iterable scopes() { + return Set.of(); + } + + @Override + public boolean isMeterEnabled(io.helidon.metrics.api.Meter.Id meterId) { + return true; + } + @Override public Clock clock() { return Clock.system(); diff --git a/metrics/api/src/main/java/io/helidon/metrics/spi/MeterRegistryFormatterProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MeterRegistryFormatterProvider.java new file mode 100644 index 00000000000..6ad838eab71 --- /dev/null +++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MeterRegistryFormatterProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.spi; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; + +/** + * Behavior for providers of meter registry formatters, which (if then can) furnish a formatter given a + * {@link io.helidon.common.media.type.MediaType}. + * + *

+ * We use a provider approach so code can obtain and run formatters that might depend heavily on particular implementations + * without the calling code having to share that heavy dependency. + *

+ */ +public interface MeterRegistryFormatterProvider { + + /** + * Returns, if possible, a {@link io.helidon.metrics.api.MeterRegistryFormatter} capable of preparing output according to + * the specified {@link io.helidon.common.media.type.MediaType}. + * @param mediaType media type of the desired output + * @param meterRegistry {@link io.helidon.metrics.api.MeterRegistry} from which to gather data + * @param scopeTagName tag name used to record scope + * @param scopeSelection scope names to format; empty means no scope-based restriction + * @param nameSelection meter names to format; empty means no name-based restriction + * @return compatible formatter; empty if none + */ + Optional formatter(MediaType mediaType, + MeterRegistry meterRegistry, + String scopeTagName, + Iterable scopeSelection, + Iterable nameSelection); +} diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java index 27b9fc4ebe3..9536ac1670a 100644 --- a/metrics/api/src/main/java/module-info.java +++ b/metrics/api/src/main/java/module-info.java @@ -28,6 +28,8 @@ requires io.helidon.builder.api; requires static io.helidon.config.metadata; + + // TODO remove next once we no longer need MP metrics APIs requires transitive microprofile.metrics.api; requires io.helidon.inject.configdriven.api; @@ -39,5 +41,7 @@ uses ExemplarService; uses io.helidon.metrics.api.MetricsProgrammaticSettings; uses io.helidon.metrics.spi.MetricsFactoryProvider; + uses io.helidon.metrics.spi.MeterRegistryFormatterProvider; + uses MetricsFactory; } diff --git a/metrics/metrics/src/main/java/io/helidon/metrics/MicrometerPrometheusFormatter.java b/metrics/metrics/src/main/java/io/helidon/metrics/MicrometerPrometheusFormatter.java index de2588ea18c..3f49a1cb8a3 100644 --- a/metrics/metrics/src/main/java/io/helidon/metrics/MicrometerPrometheusFormatter.java +++ b/metrics/metrics/src/main/java/io/helidon/metrics/MicrometerPrometheusFormatter.java @@ -42,6 +42,7 @@ * "m_" if the actual meter name starts with a digit or underscore and underscores replace special characters. *

*/ +// TODO remove this class once we've converted to use the one in helidon-metrics-micrometer. public class MicrometerPrometheusFormatter { /** * Mapping from supported media types to the corresponding Prometheus registry content types. diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java index aba5d1cca43..ee9786c37c2 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.java @@ -85,6 +85,9 @@ private Builder(String name, DistributionStatisticsConfig config) { .orElse(DEFAULT.getMinimumExpectedValueAsDouble())) .maximumExpectedValue(config.maximumExpectedValue() .orElse(DEFAULT.getMaximumExpectedValueAsDouble()))); + prep(delegate()::tags, + delegate()::description, + delegate()::baseUnit); } @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java index 57a1254327b..5fb90ccba25 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java @@ -17,8 +17,11 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; import io.helidon.metrics.api.Clock; @@ -122,6 +125,9 @@ private static MeterRegistry ensurePrometheusRegistryIsPresent(MeterRegistry met private final Clock clock; private final ConcurrentHashMap meters = new ConcurrentHashMap<>(); + private final Map scopes = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final String scopeTagName; private MMeterRegistry(MeterRegistry delegate, Clock clock, @@ -135,21 +141,44 @@ private MMeterRegistry(MeterRegistry delegate, if (!globalTags.isEmpty()) { delegate.config().meterFilter(MeterFilter.commonTags(Util.tags(globalTags))); } + + scopeTagName = metricsConfig.scopeTagName(); + MeterFilter scopeTagAdder = new MeterFilter() { + @Override + public Meter.Id map(Meter.Id id) { + return id.getTag(scopeTagName) == null + ? id.withTag(Tag.of(scopeTagName, "application")) + : id; + } + }; + + delegate.config().meterFilter(scopeTagAdder); } @Override - public List meters() { + public List meters() { return meters.values().stream().toList(); } @Override - public Collection meters(Predicate filter) { + public Collection meters(Predicate filter) { return meters.values() .stream() .filter(filter) .toList(); } + @Override + public Iterable scopes() { + return scopes.keySet(); + } + + // TODO enhance after adding back the filtering config + @Override + public boolean isMeterEnabled(io.helidon.metrics.api.Meter.Id meterId) { + return true; + } + @Override public Clock clock() { return clock; @@ -164,9 +193,12 @@ HB extends io.helidon.metrics.api.Meter.Builder> HM getOrCreate(HB build // Each type of builder declares its own so we need to decide here which specific one to invoke. // That's so we can invoke the Micrometer builder's register method, which acts as // get-or-create. + // Micrometer's register methods will throw an IllegalArgumentException if the caller specifies a builder that finds // a previously-registered meter of a different type from that implied by the builder. + // Also, the register methods actually are get-or-register. + Meter meter; // TODO Convert to switch instanceof expressions once checkstyle understand the syntax. if (builder instanceof MCounter.Builder cBuilder) { @@ -260,24 +292,54 @@ private Optional internalRemove(String name, } private void recordAdd(Meter addedMeter) { - if (addedMeter instanceof Counter counter) { - meters.put(addedMeter, MCounter.create(counter)); - } else if (addedMeter instanceof DistributionSummary summary) { - meters.put(addedMeter, MDistributionSummary.create(summary)); - } else if (addedMeter instanceof Gauge gauge) { - meters.put(addedMeter, MGauge.create(gauge)); - } else if (addedMeter instanceof Timer timer) { - meters.put(addedMeter, MTimer.create(timer)); - } else { - LOGGER.log(System.Logger.Level.WARNING, - "Attempt to record addition of unrecognized meter type " + addedMeter.getClass().getName()); + lock.lock(); + try { + Meter meter = null; + if (addedMeter instanceof Counter counter) { + meter = addedMeter; + meters.put(addedMeter, MCounter.create(counter)); + } else if (addedMeter instanceof DistributionSummary summary) { + meter = addedMeter; + meters.put(addedMeter, MDistributionSummary.create(summary)); + } else if (addedMeter instanceof Gauge gauge) { + meter = addedMeter; + meters.put(addedMeter, MGauge.create(gauge)); + } else if (addedMeter instanceof Timer timer) { + meter = addedMeter; + meters.put(addedMeter, MTimer.create(timer)); + } else { + LOGGER.log(System.Logger.Level.WARNING, + "Attempt to record addition of unrecognized meter type " + addedMeter.getClass().getName()); + } + if (meter != null) { + String scope = meter.getId().getTag(scopeTagName); + if (scope != null && !scope.isBlank()) { + AtomicInteger metersInScope = scopes.computeIfAbsent(scope, v -> new AtomicInteger()); + metersInScope.incrementAndGet(); + } + } + } finally { + lock.unlock(); } } private void recordRemove(Meter removedMeter) { - io.helidon.metrics.api.Meter removedNeutralMeter = meters.remove(removedMeter); - if (removedNeutralMeter == null) { - LOGGER.log(System.Logger.Level.WARNING, "No matching neutral meter for implementation meter " + removedMeter); + lock.lock(); + try { + io.helidon.metrics.api.Meter removedNeutralMeter = meters.remove(removedMeter); + if (removedNeutralMeter == null) { + LOGGER.log(System.Logger.Level.WARNING, "No matching neutral meter for implementation meter " + removedMeter); + } else { + String scope = removedMeter.getId().getTag(scopeTagName); + if (scope != null && !scope.isBlank()) { + AtomicInteger metersInScope = scopes.get(scope); + if (metersInScope != null) { + metersInScope.decrementAndGet(); + } + } + } + } finally { + lock.unlock(); } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java index ba1656b82bb..6c6ca8fdf51 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java @@ -167,6 +167,9 @@ static class Builder extends MMeter.Builder null); } @Override diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java index b73e7c8c7ce..26b859487fb 100644 --- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java @@ -150,4 +150,41 @@ public Tag tagCreate(String key, String value) { public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) { return MHistogramSnapshot.create(io.micrometer.core.instrument.distribution.HistogramSnapshot.empty(count, total, max)); } +// TODO remove commented code below when it's been transplanted + +// @Override +// public Optional scrape(MeterRegistry meterRegistry, +// MediaType mediaType, +// Iterable scopeSelection, +// Iterable meterNameSelection) { +// if (mediaType.equals(MediaTypes.TEXT_PLAIN) || mediaType.equals(MediaTypes.APPLICATION_OPENMETRICS_TEXT)) { +// var formatter = +// MicrometerPrometheusFormatter +// .builder(meterRegistry) +// .resultMediaType(mediaType) +// .scopeTagName(MetricsProgrammaticSettings.instance().scopeTagName()) +// .scopeSelection(scopeSelection) +// .meterNameSelection(meterNameSelection) +// .build(); +// +// return formatter.filteredOutput(); +// } else if (mediaType.equals(MediaTypes.APPLICATION_JSON)) { +// var formatter = JsonFormatter.builder(meterRegistry) +// .scopeTagName(MetricsProgrammaticSettings.instance().scopeTagName()) +// .scopeSelection(scopeSelection) +// .meterNameSelection(meterNameSelection) +// .build(); +// return formatter.data(true); +// } +// throw new UnsupportedOperationException(); +// } +// +// // TODO return something better +// @Override +// public Optional scrapeMetadata(MeterRegistry meterRegistry, +// MediaType mediaType, +// Iterable scopeSelection, +// Iterable meterNameSelection) { +// return Optional.empty(); +// } } diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatter.java new file mode 100644 index 00000000000..e1c55ef90ad --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatter.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; +import io.micrometer.prometheus.PrometheusMeterRegistry; +import io.prometheus.client.exporter.common.TextFormat; + +/** + * Retrieves and prepares meter output from the specified meter registry according to the formats supported by the Prometheus + * meter registry. + *

+ * Because the Prometheus exposition format is flat, and because some meter types have multiple values, the meter names + * in the output repeat the actual meter name with suffixes to indicate the specific quantities (e.g., + * count, total, max) each reported value conveys. Further, meter names in the output might need the prefix + * "m_" if the actual meter name starts with a digit or underscore and underscores replace special characters. + *

+ */ +class MicrometerPrometheusFormatter implements MeterRegistryFormatter { + /** + * Mapping from supported media types to the corresponding Prometheus registry content types. + */ + public static final Map MEDIA_TYPE_TO_FORMAT = Map.of( + MediaTypes.TEXT_PLAIN, TextFormat.CONTENT_TYPE_004, + MediaTypes.APPLICATION_OPENMETRICS_TEXT, TextFormat.CONTENT_TYPE_OPENMETRICS_100); + + /** + * Returns a new builder for constructing a formatter. + * + * @param meterRegistry the {@link io.helidon.metrics.api.MeterRegistry} from which to build the Prometheus output + * @return new builder + */ + public static Builder builder(MeterRegistry meterRegistry) { + return new Builder(meterRegistry); + } + + private final String scopeTagName; + private final Iterable scopeSelection; + private final Iterable meterNameSelection; + private final MediaType resultMediaType; + private final MeterRegistry meterRegistry; + + private MicrometerPrometheusFormatter(Builder builder) { + scopeTagName = builder.scopeTagName; + scopeSelection = builder.scopeSelection; + meterNameSelection = builder.meterNameSelection; + resultMediaType = builder.resultMediaType; + meterRegistry = Objects.requireNonNullElseGet(builder.meterRegistry, + io.helidon.metrics.api.Metrics::globalRegistry); + } + + /** + * Returns the Prometheus output governed by the previously-specified media type, optionally filtered + * by the previously-specified scope and meter name selections. + * + * @return filtered Prometheus output + */ + @Override + public Optional format() { + + Optional prometheusMeterRegistry = prometheusMeterRegistry(meterRegistry); + if (prometheusMeterRegistry.isPresent()) { + + // Scraping the Prometheus registry lets us limit the output to include only specified names. + Set meterNamesOfInterest = meterNamesOfInterest(prometheusMeterRegistry.get(), + scopeSelection, + meterNameSelection); + if (meterNamesOfInterest.isEmpty()) { + return Optional.empty(); + } + + String prometheusOutput = filter(prometheusMeterRegistry.get() + .scrape(MicrometerPrometheusFormatter.MEDIA_TYPE_TO_FORMAT.get( + resultMediaType), + meterNamesOfInterest)); + + return prometheusOutput.isBlank() ? Optional.empty() : Optional.of(prometheusOutput); + } + return Optional.empty(); + } + + @Override + public Optional formatMetadata() { + return Optional.empty(); + } + + /** + * Prepares a set containing the names of meters from the specified Prometheus meter registry which match + * the specified scope and meter name selections. + *

+ * For meters with multiple values, the Prometheus registry essentially creates and actually displays in its output + * additional or "child" meters. A child meter's name is the parent's name plus a suffix consisting + * of the child meter's units (if any) plus the child name. For example, the timer {@code myDelay} has child meters + * {@code myDelay_seconds_count}, {@code myDelay_seconds_sum}, and {@code myDelay_seconds_max}. (The output contains + * repetitions of the parent meter's name for each quantile, but that does not affect the meter names we need to ask + * the Prometheus meter registry to retrieve for us when we scrape.) + *

+ *

+ * We interpret any name selection passed to this method as specifying a parent name. We can ask the Prometheus meter + * registry to select specific meters by meter name when we scrape, but we need to pass it an expanded name selection that + * includes the relevant child meter names as well as the parent name. One way to choose those is first to collect the + * names from the Prometheus meter registry itself and derive the names to have the meter registry select by from those + * matching meters, their units, etc. + *

+ * + * @param prometheusMeterRegistry Prometheus meter registry to query + * @param scopeSelection scope names to select + * @param meterNameSelection meter names to select + * @return set of matching meter names (with units and suffixes as needed) to match the names as stored in the meter registry + */ + Set meterNamesOfInterest(PrometheusMeterRegistry prometheusMeterRegistry, + Iterable scopeSelection, + Iterable meterNameSelection) { + + Set result = new HashSet<>(); + + var scopes = new HashSet<>(); + scopeSelection.forEach(scopes::add); + + var names = new HashSet<>(); + meterNameSelection.forEach(names::add); + + Predicate scopePredicate = scopes.isEmpty() || scopeTagName == null || scopeTagName.isBlank() + ? m -> true + : m -> scopes.contains(m.getId().getTag(scopeTagName)); + + Predicate namePredicate = names.isEmpty() ? n -> true : names::contains; + + for (Meter meter : prometheusMeterRegistry.getMeters()) { + String meterName = meter.getId().getName(); + if (!namePredicate.test(meterName)) { + continue; + } + Set allUnitsForMeterName = new HashSet<>(); + allUnitsForMeterName.add(""); + Set allSuffixesForMeterName = new HashSet<>(); + allSuffixesForMeterName.add(""); + + prometheusMeterRegistry.find(meterName) + .meters() + .forEach(m -> { + Meter.Id meterId = m.getId(); + if (scopePredicate.test(m)) { + allUnitsForMeterName.add("_" + normalizeUnit(meterId.getBaseUnit())); + allSuffixesForMeterName.addAll(meterNameSuffixes(meterId.getType())); + } + }); + + String normalizedMeterName = normalizeMeterName(meterName); + + allUnitsForMeterName + .forEach(units -> allSuffixesForMeterName + .forEach(suffix -> result.add(normalizedMeterName + units + suffix))); + } + return result; + } + + /** + * Filter the Prometheus-format report. + * + * @param output Prometheus-format report + * @return output filtered + */ + private static String filter(String output) { + return output.replaceFirst("# EOF\r?\n?", ""); + } + + private static Optional prometheusMeterRegistry(MeterRegistry meterRegistry) { + io.micrometer.core.instrument.MeterRegistry mMeterRegistry = + meterRegistry.unwrap(io.micrometer.core.instrument.MeterRegistry.class); + if (mMeterRegistry instanceof CompositeMeterRegistry compositeMeterRegistry) { + return compositeMeterRegistry.getRegistries().stream() + .filter(PrometheusMeterRegistry.class::isInstance) + .findFirst() + .map(PrometheusMeterRegistry.class::cast); + } + return Optional.empty(); + } + + private static String flushForMeterAndClear(StringBuilder helpAndType, StringBuilder metricData) { + StringBuilder result = new StringBuilder(); + if (!metricData.isEmpty()) { + result.append(helpAndType.toString()) + .append(metricData); + } + helpAndType.setLength(0); + metricData.setLength(0); + return result.toString(); + } + + + /** + * Returns the Prometheus-format meter name suffixes for the given meter type. + * + * @param meterType {@link io.micrometer.core.instrument.Meter.Type} of interest + * @return suffixes used in reporting the corresponding meter's value(s) + */ + static Set meterNameSuffixes(Meter.Type meterType) { + return switch (meterType) { + case COUNTER -> Set.of("_total"); + case DISTRIBUTION_SUMMARY, LONG_TASK_TIMER, TIMER -> Set.of("_count", "_sum", "_max"); + case GAUGE, OTHER -> Set.of(); + }; + } + + /** + * Convert the meter name to the format used by the Prometheus simple client. + * + * @param meterName name of the meter + * @return normalized meter name + */ + static String normalizeMeterName(String meterName) { + String result = meterName; + + // Convert special characters to underscores. + result = result.replaceAll("[-+.!?@#$%^&*`'\\s]+", "_"); + + // Prometheus simple client adds the prefix "m_" if a meter name starts with a digit or an underscore. + if (result.matches("^[0-9_]+.*")) { + result = "m_" + result; + } + + // Replace non-identifier characters. + result = result.replaceAll("[^A-Za-z0-9_]", "_"); + + return result; + } + + private static String normalizeUnit(String unit) { + return unit == null ? "" : unit; + } + + /** + * Builder for creating a tailored Prometheus formatter. + */ + public static class Builder implements io.helidon.common.Builder { + + private Iterable meterNameSelection = Set.of(); + private String scopeTagName; + private Iterable scopeSelection = Set.of(); + private MediaType resultMediaType = MediaTypes.TEXT_PLAIN; + private MeterRegistry meterRegistry; + + /** + * Used only internally. + */ + private Builder() { + } + + private Builder(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public MicrometerPrometheusFormatter build() { + return new MicrometerPrometheusFormatter(this); + } + + /** + * Sets the meter name with which to filter the output. + * + * @param meterNameSelection meter name to select + * @return updated builder + */ + public Builder meterNameSelection(Iterable meterNameSelection) { + this.meterNameSelection = meterNameSelection; + return identity(); + } + + /** + * Sets the scope value with which to filter the output. + * + * @param scopeSelection scope to select + * @return updated builder + */ + public Builder scopeSelection(Iterable scopeSelection) { + this.scopeSelection = scopeSelection; + return identity(); + } + + /** + * Sets the scope tag name with which to filter the output. + * + * @param scopeTagName scope tag name + * @return updated builder + */ + public Builder scopeTagName(String scopeTagName) { + this.scopeTagName = scopeTagName; + return identity(); + } + + /** + * Sets the {@link io.helidon.common.media.type.MediaType} which controls the formatting of the resulting output. + * + * @param resultMediaType media type + * @return updated builder + */ + public Builder resultMediaType(MediaType resultMediaType) { + this.resultMediaType = resultMediaType; + return identity(); + } + } +} diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatterProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatterProvider.java new file mode 100644 index 00000000000..b1d7937a6ce --- /dev/null +++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerPrometheusFormatterProvider.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; +import io.helidon.metrics.spi.MeterRegistryFormatterProvider; + +/** + * Micrometer (and Prometheus, particularly) specific formatter. + */ +public class MicrometerPrometheusFormatterProvider implements MeterRegistryFormatterProvider { + @Override + public Optional formatter(MediaType mediaType, + MeterRegistry meterRegistry, + String scopeTagName, + Iterable scopeSelection, + Iterable nameSelection) { + return matches(mediaType, MediaTypes.TEXT_PLAIN) || matches(mediaType, MediaTypes.APPLICATION_OPENMETRICS_TEXT) + ? Optional.of(MicrometerPrometheusFormatter.builder(meterRegistry) + .scopeTagName(scopeTagName) + .scopeSelection(scopeSelection) + .meterNameSelection(nameSelection) + .build()) + : Optional.empty(); + } + + private static boolean matches(MediaType a, MediaType b) { + return a.type().equals(b.type()) && a.subtype().equals(b.subtype()); + } +} diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java index 8232d0e9cd2..2674ff232bf 100644 --- a/metrics/providers/micrometer/src/main/java/module-info.java +++ b/metrics/providers/micrometer/src/main/java/module-info.java @@ -15,6 +15,7 @@ */ import io.helidon.metrics.micrometer.MicrometerMetricsFactoryProvider; +import io.helidon.metrics.micrometer.MicrometerPrometheusFormatterProvider; /** * Micrometer adapter for Helidon metrics API. @@ -26,6 +27,9 @@ requires static micrometer.registry.prometheus; requires io.helidon.common; requires io.helidon.common.config; + requires io.helidon.common.media.type; + requires simpleclient.common; provides io.helidon.metrics.spi.MetricsFactoryProvider with MicrometerMetricsFactoryProvider; + provides io.helidon.metrics.spi.MeterRegistryFormatterProvider with MicrometerPrometheusFormatterProvider; } \ No newline at end of file diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestPrometheusFormatting.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestPrometheusFormatting.java new file mode 100644 index 00000000000..17a4da30f51 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestPrometheusFormatting.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.helidon.common.media.type.MediaTypes; +import io.helidon.common.testing.junit5.OptionalMatcher; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class TestPrometheusFormatting { + + private static final String SCOPE_TAG_NAME = "this-scope"; + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder() + .scopeTagName(SCOPE_TAG_NAME); + + meterRegistry = Metrics.createMeterRegistry(metricsConfigBuilder.build()); + } + + @Test + void testRetrievingAll() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c1") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "app")))); + assertThat("Initial counter value", c.count(), Matchers.is(0L)); + c.increment(); + assertThat("After increment", c.count(), Matchers.is(1L)); + + Timer d = meterRegistry.getOrCreate(Timer.builder("t1") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "other")))); + d.record(3, TimeUnit.SECONDS); + + Timer e = meterRegistry.getOrCreate(Timer.builder("t1-1") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "app")))); + e.record(2, TimeUnit.SECONDS); + + MicrometerPrometheusFormatter formatter = MicrometerPrometheusFormatter.builder(meterRegistry) + .scopeTagName(SCOPE_TAG_NAME) + .build(); + Optional output = formatter.format(); + + assertThat("Formatted output", + output, + OptionalMatcher.optionalValue( + allOf(containsString(scopeExpr("c1_total", + "this_scope", + "app", + "1.0")), + containsString(scopeExpr("t1_seconds_count", + "this_scope", + "other", + "1.0")), + containsString(scopeExpr("t1_seconds_sum", + "this_scope", + "other", + "3.0")), + containsString(scopeExpr("t1_1_seconds_count", + "this_scope", + "app", + "1.0"))))); + } + + @Test + void testRetrievingByName() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); + assertThat("Initial counter value", c.count(), Matchers.is(0L)); + c.increment(); + assertThat("After increment", c.count(), Matchers.is(1L)); + + Timer d = meterRegistry.getOrCreate(Timer.builder("t2")); + d.record(7, TimeUnit.SECONDS); + + MicrometerPrometheusFormatter formatter = MicrometerPrometheusFormatter.builder(meterRegistry) + .scopeTagName(SCOPE_TAG_NAME) + .meterNameSelection(Set.of("c2")) + .build(); + Optional output = formatter.format(); + + assertThat("Formatted output", + output, + OptionalMatcher.optionalValue( + allOf(containsString(scopeExpr("c2_total", + "this_scope", + "application", + "1.0")), + not(containsString(scopeExpr("t2_seconds_count", + "this_scope", + "application", + "1.0"))), + not(containsString(scopeExpr("t2_seconds_sum", + "this_scope", + "applicaiton", "7.0")))))); + + } + + @Test + void testRetrievingByScope() { + + Counter c = meterRegistry.getOrCreate(Counter.builder("c3") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "app")))); + assertThat("Initial counter value", c.count(), is(0L)); + c.increment(); + assertThat("After increment", c.count(), is(1L)); + + Timer d = meterRegistry.getOrCreate(Timer.builder("t3") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "other-scope")))); + d.record(7, TimeUnit.SECONDS); + + Timer e = meterRegistry.getOrCreate(Timer.builder("t3-1") + .tags(Set.of(Tag.create(SCOPE_TAG_NAME, "app")))); + e.record(2, TimeUnit.SECONDS); + + + MicrometerPrometheusFormatter formatter = MicrometerPrometheusFormatter.builder(meterRegistry) + .scopeTagName(SCOPE_TAG_NAME) + .scopeSelection(Set.of("app")) + .build(); + + Optional output = formatter.format(); + + assertThat("Formatted output", + output, + OptionalMatcher.optionalValue( + allOf(containsString(scopeExpr("c3_total", + "this_scope", + "app", + "1.0")), + not(containsString(scopeExpr("t3_seconds_count", + "this_scope", + "other-scope", + "1.0"))), + not(containsString(scopeExpr("t3_seconds_sum", + "this_scope", + "other-scope", + "3.0"))), + containsString(scopeExpr("t3_1_seconds_count", + "this_scope", + "app", + "1.0"))))); + } + + private static String scopeExpr(String meterName, String key, String value, String suffix) { + return meterName + "{" + key + "=\"" + value + "\",} " + suffix; + } +} diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestScopes.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestScopes.java new file mode 100644 index 00000000000..1a760bbace5 --- /dev/null +++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestScopes.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.metrics.micrometer; + +import java.util.Set; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class TestScopes { + + private static final String SCOPE_TAG_NAME = "my-tag"; + + + private static MeterRegistry meterRegistry; + + @BeforeAll + static void prep() { + meterRegistry = Metrics.createMeterRegistry(MetricsConfig.builder() + .scopeTagName(SCOPE_TAG_NAME) + .build()); + } + + @Test + void testScopeManagement() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); + Timer t = meterRegistry.getOrCreate(Timer.builder("t1") + .tags(Set.of(Tag.create("color", "red")))); + io.micrometer.core.instrument.Counter mCounter = c.unwrap(io.micrometer.core.instrument.Counter.class); + assertThat("Initial value", c.count(), is(0L)); + mCounter.increment(); + assertThat("Updated value", c.count(), is(1L)); + + // If scope is not explicitly set, "application" should be automatically added. + assertThat("Scopes in meter registry", meterRegistry.scopes(), allOf(contains("application"), + not(contains("color")))); + } +} diff --git a/webserver/observe/metrics/pom.xml b/webserver/observe/metrics/pom.xml index 28dfe9e92a8..89263aa604c 100644 --- a/webserver/observe/metrics/pom.xml +++ b/webserver/observe/metrics/pom.xml @@ -53,6 +53,24 @@ io.helidon.common helidon-common-context + + io.micrometer + micrometer-core + provided + true + + + io.micrometer + micrometer-registry-prometheus + provided + true + + + io.prometheus + simpleclient + provided + true + @@ -79,6 +97,16 @@ io.helidon.config helidon-config-metadata + + io.helidon.metrics + helidon-metrics-micrometer + test + + + io.helidon.common.testing + helidon-common-testing-junit5 + test + org.junit.jupiter junit-jupiter-api diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonFormatter.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonFormatter.java new file mode 100644 index 00000000000..bf51b790d50 --- /dev/null +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonFormatter.java @@ -0,0 +1,502 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.webserver.observe.metrics; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.DistributionSummary; +import io.helidon.metrics.api.Meter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; +import io.helidon.metrics.api.Registry; +import io.helidon.metrics.api.RegistryFactory; +import io.helidon.metrics.api.Timer; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; + +/** + * JSON formatter for a meter registry (independent of the underlying registry implementation). + */ +class JsonFormatter implements MeterRegistryFormatter { + + /** + * Returns a new builder for a formatter. + * + * @return new builder + */ + static JsonFormatter.Builder builder(MeterRegistry meterRegistry) { + return new Builder(meterRegistry); + } + + private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Map.of()); + + private static final Map JSON_ESCAPED_CHARS_MAP = initEscapedCharsMap(); + + private static final Pattern JSON_ESCAPED_CHARS_REGEX = Pattern + .compile(JSON_ESCAPED_CHARS_MAP + .keySet() + .stream() + .map(Pattern::quote) + .collect(Collectors.joining("", "[", "]"))); + + + private final Iterable meterNameSelection; + private final Iterable scopeSelection; + private final String scopeTagName; + private final MeterRegistry meterRegistry; + + private JsonFormatter(Builder builder) { + meterNameSelection = builder.meterNameSelection; + scopeSelection = builder.scopeSelection; + scopeTagName = builder.scopeTagName; + meterRegistry = builder.meterRegistry; + } + + /** + * Returns a JSON object conveying all the meter identification and data (but no metadata), organized by scope. + * + * @return meter data + */ + @Override + public Optional format() { + + boolean organizeByScope = shouldOrganizeByScope(); + + Map> meterOutputBuildersByScope = organizeByScope ? new HashMap<>() : null; + Map meterOutputBuildersIgnoringScope = organizeByScope ? null : new HashMap<>(); + + /* + * If we organize by multiple scopes, then meterOutputBuildersByScope will have one top-level entry per scope we find, + * keyed by the scope name. We will gather the output for the metrics in each scope under a JSON node for that scope. + * + * If the scope selection accepts only one scope, or if we are NOT organizing by scopes, then we don't use that map and + * instead use meterOutputBuildersIgnoringScope to gather all JSON for the meters under the same parent. + * + * The JSON output format has one "flat" entry for each single-valued meter--counter or gauge--with the JSON + * key set to the name and tags from the meter ID and the JSON value reporting the single value. + * + * In contrast, the JSON output has a "structured" entry for each multi-valued meter--distribution summary or timer. + * The JSON node key is the name only--no tags--from the meter ID, and the corresponding JSON structure has a child + * for each distinct value-name plus tags group. + * + * Here is an example: + * + { + "carsCounter;car=suv;colour=red": 0, + "carsCounter;car=sedan;colour=blue": 0, + "carsTimer": { + "count;colour=red": 0, + "sum;colour=red": 0.0, + "max;colour=red": 0.0, + "count;colour=blue": 0, + "sum;colour=blue": 0.0, + "max;colour=blue": 0.0 + } + } + */ + + + var scopes = new HashSet<>(); + scopeSelection.forEach(scopes::add); + + var names = new HashSet<>(); + meterNameSelection.forEach(names::add); + + Predicate namePredicate = names.isEmpty() ? n -> true : names::contains; + + AtomicBoolean isAnyOutput = new AtomicBoolean(false); + + meterRegistry.scopes().forEach(scope -> { + String matchingScope = matchingScope(scope); + if (matchingScope != null) { + meterRegistry.meters().forEach(meter -> { + if (meterRegistry.isMeterEnabled(meter.id()) && namePredicate.test(meter.id().name())) { + if (matchesName(meter.id().name())) { + + Map meterOutputBuildersWithinParent = + organizeByScope ? meterOutputBuildersByScope + .computeIfAbsent(matchingScope, + ms -> new HashMap<>()) + : meterOutputBuildersIgnoringScope; + + // Find the output builder for the key relevant to this meter and then add this meter's contribution + // to the output. + MetricOutputBuilder metricOutputBuilder = meterOutputBuildersWithinParent + .computeIfAbsent(metricOutputKey(meter), + k -> MetricOutputBuilder.create(meter)); + metricOutputBuilder.add(meter); + isAnyOutput.set(true); + + } + } + }); + } + }); + + JsonObjectBuilder top = JSON.createObjectBuilder(); + if (organizeByScope) { + meterOutputBuildersByScope.forEach((scope, outputBuilders) -> { + JsonObjectBuilder scopeBuilder = JSON.createObjectBuilder(); + outputBuilders.forEach((key, outputBuilder) -> outputBuilder.apply(scopeBuilder)); + top.add(scope, scopeBuilder); + }); + } else { + meterOutputBuildersIgnoringScope.forEach((key, outputBuilder) -> outputBuilder.apply(top)); + } + + return isAnyOutput.get() ? Optional.of(top.build()) : Optional.empty(); + } + + @Override + public Optional formatMetadata() { + + boolean organizeByScope = shouldOrganizeByScope(); + + Map> metadataOutputBuildersByScope = organizeByScope ? new HashMap<>() : null; + Map metadataOutputBuildersIgnoringScope = organizeByScope ? null : new HashMap<>(); + + AtomicBoolean isAnyOutput = new AtomicBoolean(false); + RegistryFactory registryFactory = RegistryFactory.getInstance(); + registryFactory.scopes().forEach(scope -> { + String matchingScope = matchingScope(scope); + if (matchingScope != null) { + Registry registry = registryFactory.getRegistry(scope); + registry.getMetadata().forEach((name, metadata) -> { + if (matchesName(name)) { + + Map metadataOutputBuilderWithinParent = + organizeByScope ? metadataOutputBuildersByScope + .computeIfAbsent(matchingScope, ms -> new HashMap<>()) + : metadataOutputBuildersIgnoringScope; + + JsonObjectBuilder builderForThisName = metadataOutputBuilderWithinParent + .computeIfAbsent(name, k -> JSON.createObjectBuilder()); + addNonEmpty(builderForThisName, "unit", metadata.getUnit()); + addNonEmpty(builderForThisName, "description", metadata.getDescription()); + isAnyOutput.set(true); + + List> tagGroups = new ArrayList<>(); + + registry.metricIdsByName(name).forEach(metricId -> { + if (registry.enabled(name)) { + List tags = metricId.getTags().entrySet().stream() + .map(entry -> jsonEscape(entry.getKey()) + "=" + jsonEscape(entry.getValue())) + .toList(); + if (!tags.isEmpty()) { + tagGroups.add(tags); + } + } + }); + if (!tagGroups.isEmpty()) { + JsonArrayBuilder tagsOverAllMetricsWithSameName = JSON.createArrayBuilder(); + for (List tagGroup : tagGroups) { + JsonArrayBuilder tagsForMetricBuilder = JSON.createArrayBuilder(); + tagGroup.forEach(tagsForMetricBuilder::add); + tagsOverAllMetricsWithSameName.add(tagsForMetricBuilder); + } + builderForThisName.add("tags", tagsOverAllMetricsWithSameName); + isAnyOutput.set(true); + } + } + }); + } + }); + JsonObjectBuilder top = JSON.createObjectBuilder(); + if (organizeByScope) { + metadataOutputBuildersByScope.forEach((scope, builders) -> { + JsonObjectBuilder scopeBuilder = JSON.createObjectBuilder(); + builders.forEach(scopeBuilder::add); + top.add(scope, scopeBuilder); + }); + } else { + metadataOutputBuildersIgnoringScope.forEach(top::add); + } + return isAnyOutput.get() ? Optional.of(top.build()) : Optional.empty(); + } + + static String jsonEscape(String s) { + final Matcher m = JSON_ESCAPED_CHARS_REGEX.matcher(s); + final StringBuilder sb = new StringBuilder(); + while (m.find()) { + m.appendReplacement(sb, JSON_ESCAPED_CHARS_MAP.get(m.group())); + } + m.appendTail(sb); + return sb.toString(); + } + + + private static Map initEscapedCharsMap() { + final Map result = new HashMap<>(); + result.put("\b", bsls("b")); + result.put("\f", bsls("f")); + result.put("\n", bsls("n")); + result.put("\r", bsls("r")); + result.put("\t", bsls("t")); + result.put("\"", bsls("\"")); + result.put("\\", bsls("\\\\")); + result.put(";", "_"); + return result; + } + + private static String bsls(String s) { + return "\\\\" + s; + } + + private static void addNonEmpty(JsonObjectBuilder builder, String name, String value) { + if ((null != value) && !value.isEmpty()) { + builder.add(name, value); + } + } + + private boolean shouldOrganizeByScope() { + var it = scopeSelection.iterator(); + if (it.hasNext()) { + it.next(); + // return false if exactly one selection; true if at least two. + return it.hasNext(); + } + return true; + } + + private static String metricOutputKey(Meter meter) { + return meter instanceof Counter || meter instanceof io.helidon.metrics.api.Gauge + ? flatNameAndTags(meter.id()) + : structureName(meter.id()); + } + + private static String flatNameAndTags(Meter.Id meterId) { + StringJoiner sj = new StringJoiner(";"); + sj.add(meterId.name()); + meterId.tags().forEach(tag -> sj.add(tag.key() + "=" + tag.value())); + return sj.toString(); + } + + private static String structureName(Meter.Id meterId) { + return meterId.name(); + } + + private String matchingScope(String scope) { + Iterator scopeIterator = scopeSelection.iterator(); + if (!scopeIterator.hasNext()) { + return scope; + } + if (scope == null) { + return null; + } + + while (scopeIterator.hasNext()) { + if (scopeIterator.next().equals(scope)) { + return scope; + } + } + return null; + } + + private boolean matchesName(String metricName) { + Iterator nameIterator = meterNameSelection.iterator(); + if (!nameIterator.hasNext()) { + return true; + } + while (nameIterator.hasNext()) { + if (nameIterator.next().equals(metricName)) { + return true; + } + } + return false; + } + + private abstract static class MetricOutputBuilder { + + private static MetricOutputBuilder create(Meter meter) { + return meter instanceof Counter + || meter instanceof io.helidon.metrics.api.Gauge + ? new Flat(meter) + : new Structured(meter); + } + + + + private final Meter meter; + + protected MetricOutputBuilder(Meter meter) { + this.meter = meter; + } + + protected Meter meter() { + return meter; + } + + protected abstract void add(Meter meter); + protected abstract void apply(JsonObjectBuilder builder); + + private static class Flat extends MetricOutputBuilder { + + private Flat(Meter meter) { + super(meter); + } + + @Override + protected void apply(JsonObjectBuilder builder) { + if (meter() instanceof Counter counter) { + builder.add(flatNameAndTags(meter().id()), counter.count()); + return; + } + if (meter() instanceof io.helidon.metrics.api.Gauge gauge) { + + String nameWithTags = flatNameAndTags(meter().id()); + builder.add(nameWithTags, gauge.value()); + return; + } + throw new IllegalArgumentException("Attempt to format meter with structured data as flat JSON " + + meter().getClass().getName()); + } + + @Override + protected void add(Meter meter) { + } + } + + private static class Structured extends MetricOutputBuilder { + + private final List children = new ArrayList<>(); + private final JsonObjectBuilder sameNameBuilder = JSON.createObjectBuilder(); + + Structured(Meter meter) { + super(meter); + } + + @Override + protected void add(Meter meter) { + if (!meter().getClass().isInstance(meter)) { + throw new IllegalArgumentException( + String.format("Attempt to add meter of type %s to existing output for a meter of type %s", + meter.getClass().getName(), + meter().getClass().getName())); + } + children.add(meter); + } + + @Override + protected void apply(JsonObjectBuilder builder) { + Meter.Id meterId = meter().id(); + children.forEach(child -> { + Meter.Id childID = child.id(); + + if (meter() instanceof DistributionSummary summary) { + DistributionSummary typedChild = (DistributionSummary) child; + sameNameBuilder.add(valueId("count", childID), typedChild.count()); + sameNameBuilder.add(valueId("max", childID), typedChild.snapshot().max()); + sameNameBuilder.add(valueId("mean", childID), typedChild.snapshot().mean()); + sameNameBuilder.add(valueId("total", childID), typedChild.totalAmount()); + } else if (meter() instanceof Timer timer) { + Timer typedChild = (Timer) child; + sameNameBuilder.add(valueId("count", childID), typedChild.count()); + sameNameBuilder.add(valueId("elapsedTime", childID), typedChild.totalTime(TimeUnit.SECONDS)); + sameNameBuilder.add(valueId("max", childID), typedChild.max(TimeUnit.SECONDS)); + sameNameBuilder.add(valueId("mean", childID), typedChild.mean(TimeUnit.SECONDS)); + } else { + throw new IllegalArgumentException("Unrecognized meter type " + + meter().getClass().getName()); + } + }); + builder.add(meterId.name(), sameNameBuilder); + } + + + private static String valueId(String valueName, Meter.Id meterId) { + return valueName + tagsPortion(meterId); + } + + private static String tagsPortion(Meter.Id metricID) { + StringJoiner sj = new StringJoiner(";", ";", ""); + sj.setEmptyValue(""); + metricID.tags().forEach(tag -> sj.add(tag.key() + "=" + tag.value())); + return sj.toString(); + } + } + } + + static class Builder implements io.helidon.common.Builder { + + private final MeterRegistry meterRegistry; + private Iterable meterNameSelection = Set.of(); + private String scopeTagName; + private Iterable scopeSelection = Set.of(); + + /** + * Used only internally. + */ + private Builder(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + public JsonFormatter build() { + return new JsonFormatter(this); + } + + /** + * Sets the meter name with which to filter the output. + * + * @param meterNameSelection meter name to select + * @return updated builder + */ + public Builder meterNameSelection(Iterable meterNameSelection) { + this.meterNameSelection = meterNameSelection; + return identity(); + } + + /** + * Sets the scope value with which to filter the output. + * + * @param scopeSelection scope to select + * @return updated builder + */ + public Builder scopeSelection(Iterable scopeSelection) { + this.scopeSelection = scopeSelection; + return identity(); + } + + /** + * Sets the scope tag name with which to filter the output. + * + * @param scopeTagName scope tag name + * @return updated builder + */ + public Builder scopeTagName(String scopeTagName) { + this.scopeTagName = scopeTagName; + return identity(); + } + } +} diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonMeterRegistryFormatterProvider.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonMeterRegistryFormatterProvider.java new file mode 100644 index 00000000000..93cfae95e0a --- /dev/null +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/JsonMeterRegistryFormatterProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.webserver.observe.metrics; + +import java.util.Optional; + +import io.helidon.common.media.type.MediaType; +import io.helidon.common.media.type.MediaTypes; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; +import io.helidon.metrics.spi.MeterRegistryFormatterProvider; + +/** + * JSON formatter provider. + */ +public class JsonMeterRegistryFormatterProvider implements MeterRegistryFormatterProvider { + + @Override + public Optional formatter(MediaType mediaType, + MeterRegistry meterRegistry, + String scopeTagName, + Iterable scopeSelection, + Iterable nameSelection) { + return mediaType.type().equals(MediaTypes.APPLICATION_JSON.type()) + && mediaType.subtype().equals(MediaTypes.APPLICATION_JSON.subtype()) + ? Optional.of(JsonFormatter.builder(meterRegistry) + .scopeTagName(scopeTagName) + .scopeSelection(scopeSelection) + .meterNameSelection(nameSelection) + .build()) + : Optional.empty(); + } +} diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java index e32cbfa60ae..9c0a4c633ee 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/KeyPerformanceIndicatorMetricsImpls.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig; import io.helidon.metrics.api.KeyPerformanceIndicatorMetricsSettings; import io.helidon.metrics.api.Registry; import io.helidon.metrics.api.RegistryFactory; @@ -68,6 +69,7 @@ class KeyPerformanceIndicatorMetricsImpls { private KeyPerformanceIndicatorMetricsImpls() { } + // TODO remove /** * Provides a KPI metrics instance. * @@ -83,6 +85,21 @@ static KeyPerformanceIndicatorSupport.Metrics get(String metricsNamePrefix, : new Basic(metricsNamePrefix)); } + /** + * Provides a KPI metrics instance. + * + * @param metricsNamePrefix prefix to use for the created metrics + * @param kpiConfig KPI metrics config which may influence the construction of the metrics + * @return properly prepared new KPI metrics instance + */ + static KeyPerformanceIndicatorSupport.Metrics get(String metricsNamePrefix, + KeyPerformanceIndicatorMetricsConfig kpiConfig) { + return KPI_METRICS.computeIfAbsent(metricsNamePrefix, prefix -> + kpiConfig.isExtended() + ? new Extended(metricsNamePrefix, kpiConfig) + : new Basic(metricsNamePrefix)); + } + /** * Basic KPI metrics. */ @@ -134,9 +151,18 @@ private static class Extended extends Basic { protected static final String LOAD_DESCRIPTION = "Measures the total number of in-flight requests and rates at which they occur"; + // TODO remove protected Extended(String metricsNamePrefix, KeyPerformanceIndicatorMetricsSettings kpiConfig) { + this(metricsNamePrefix, kpiConfig.longRunningRequestThresholdMs()); + } + + protected Extended(String metricsNamePrefix, KeyPerformanceIndicatorMetricsConfig kpiConfig) { + this(metricsNamePrefix, kpiConfig.longRunningRequestThresholdMs()); + } + + private Extended(String metricsNamePrefix, long longRunningRequestThresholdMs) { super(metricsNamePrefix); - longRunningRequestThresdholdMs = kpiConfig.longRunningRequestThresholdMs(); + this.longRunningRequestThresdholdMs = longRunningRequestThresholdMs; inflightRequests = kpiMetricRegistry().gauge(Metadata.builder() .withName(metricsNamePrefix + INFLIGHT_REQUESTS_NAME) diff --git a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java index a77de34328b..f897be0c078 100644 --- a/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java +++ b/webserver/observe/metrics/src/main/java/io/helidon/webserver/observe/metrics/MetricsFeature.java @@ -15,22 +15,31 @@ */ package io.helidon.webserver.observe.metrics; +import java.lang.System.Logger.Level; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.ServiceLoader; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Stream; +import io.helidon.common.HelidonServiceLoader; import io.helidon.common.LazyValue; import io.helidon.common.media.type.MediaType; import io.helidon.common.media.type.MediaTypes; import io.helidon.config.Config; import io.helidon.config.metadata.ConfiguredOption; import io.helidon.http.Http; -import io.helidon.metrics.api.MetricsSettings; +import io.helidon.metrics.api.KeyPerformanceIndicatorMetricsConfig; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.MeterRegistryFormatter; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.MetricsFactory; import io.helidon.metrics.api.Registry; import io.helidon.metrics.api.RegistryFactory; -import io.helidon.metrics.api.SystemTagsManager; +import io.helidon.metrics.spi.MeterRegistryFormatterProvider; import io.helidon.webserver.KeyPerformanceIndicatorSupport; import io.helidon.webserver.http.Handler; import io.helidon.webserver.http.HttpRouting; @@ -40,12 +49,17 @@ import io.helidon.webserver.http.ServerResponse; import io.helidon.webserver.servicecommon.HelidonFeatureSupport; +import static io.helidon.http.Http.HeaderNames.ALLOW; +import static io.helidon.http.Http.Status.METHOD_NOT_ALLOWED_405; +import static io.helidon.http.Http.Status.NOT_ACCEPTABLE_406; +import static io.helidon.http.Http.Status.NOT_FOUND_404; +import static io.helidon.http.Http.Status.OK_200; + /** * Support for metrics for Helidon WebServer. * *

- * By defaults creates the /metrics endpoint with three sub-paths: application, - * vendor and base. + * By default creates the /metrics endpoint. *

* To register with web server: *

{@code
@@ -72,19 +86,20 @@
  */
 public class MetricsFeature extends HelidonFeatureSupport {
     private static final System.Logger LOGGER = System.getLogger(MetricsFeature.class.getName());
-    private static final Handler DISABLED_ENDPOINT_HANDLER = (req, res) -> res.status(Http.Status.NOT_FOUND_404)
+    private static final Handler DISABLED_ENDPOINT_HANDLER = (req, res) -> res.status(NOT_FOUND_404)
             .send("Metrics are disabled");
 
     private static final Iterable EMPTY_ITERABLE = Collections::emptyIterator;
-    private final MetricsSettings metricsSettings;
-    private final RegistryFactory registryFactory;
+    private final MetricsConfig metricsConfig;
+    private final MetricsFactory metricsFactory;
+    private final MeterRegistry meterRegistry;
 
     private MetricsFeature(Builder builder) {
         super(LOGGER, builder, "Metrics");
 
-        this.registryFactory = builder.registryFactory();
-        this.metricsSettings = builder.metricsSettings();
-        SystemTagsManager.create(metricsSettings);
+        this.metricsConfig = builder.metricsConfigBuilder.build();
+        this.metricsFactory = builder.metricsFactory.get();
+        meterRegistry = Objects.requireNonNullElseGet(builder.meterRegistry, metricsFactory::globalRegistry);
     }
 
     /**
@@ -124,7 +139,7 @@ public static Builder builder() {
     public Optional service() {
         // main service is responsible for exposing metrics endpoints over HTTP
         return Optional.of(rules -> {
-            if (registryFactory.enabled()) {
+            if (metricsConfig.enabled()) {
                 setUpEndpoints(rules);
             } else {
                 setUpDisabledEndpoints(rules);
@@ -142,8 +157,9 @@ public void configureVendorMetrics(HttpRouting.Builder rules) {
 
         KeyPerformanceIndicatorSupport.Metrics kpiMetrics =
                 KeyPerformanceIndicatorMetricsImpls.get(metricPrefix,
-                                                        metricsSettings
-                                                                .keyPerformanceIndicatorSettings());
+                                                        metricsConfig
+                                                                .keyPerformanceIndicatorMetricsConfig()
+                                                                .orElseGet(KeyPerformanceIndicatorMetricsConfig::create));
 
         rules.addFilter((chain, req, res) -> {
             KeyPerformanceIndicatorSupport.Context kpiContext = kpiContext(req);
@@ -160,20 +176,6 @@ public void configureVendorMetrics(HttpRouting.Builder rules) {
         });
     }
 
-    @Override
-    public void beforeStart() {
-        if (registryFactory.enabled()) {
-            registryFactory.start();
-        }
-    }
-
-    @Override
-    public void afterStop() {
-        if (registryFactory.enabled()) {
-            registryFactory.stop();
-        }
-    }
-
     @Override
     protected void context(String context) {
         super.context(context);
@@ -185,6 +187,42 @@ protected void postSetup(HttpRouting.Builder defaultRouting, HttpRouting.Builder
         RegistryFactory.getInstance().getRegistry(Registry.BASE_SCOPE); // to trigger lazy creation if it's not already done.
     }
 
+    Optional output(MediaType mediaType,
+                       Iterable scopeSelection,
+                       Iterable nameSelection) {
+        MeterRegistryFormatter formatter = chooseFormatter(meterRegistry,
+                                                           mediaType,
+                                                           metricsConfig.scopeTagName(),
+                                                           scopeSelection,
+                                                           nameSelection);
+
+        return formatter.format();
+    }
+
+    private MeterRegistryFormatter chooseFormatter(MeterRegistry meterRegistry,
+                                                   MediaType mediaType,
+                                                   String scopeTagName,
+                                                   Iterable scopeSelection,
+                                                   Iterable nameSelection) {
+        Optional formatter = HelidonServiceLoader.builder(
+                        ServiceLoader.load(MeterRegistryFormatterProvider.class))
+                .build()
+                .stream()
+                .map(provider -> provider.formatter(mediaType,
+                                                    meterRegistry,
+                                                    scopeTagName,
+                                                    scopeSelection,
+                                                    nameSelection))
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .findFirst();
+
+        if (formatter.isPresent()) {
+            return formatter.get();
+        }
+        throw new UnsupportedOperationException("Unable to find a meter registry formatter for media type " + mediaType);
+    }
+
     private void getAll(ServerRequest req, ServerResponse res) {
         getMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of));
     }
@@ -196,26 +234,37 @@ private void getMatching(ServerRequest req,
         MediaType mediaType = bestAccepted(req);
         res.header(Http.Headers.CACHE_NO_CACHE);
         if (mediaType == null) {
-            res.status(Http.Status.NOT_ACCEPTABLE_406);
+            res.status(NOT_ACCEPTABLE_406);
             res.send();
         }
 
+        getOrOptionsMatching(mediaType, res, () -> output(mediaType,
+                                                          scopeSelection,
+                                                          nameSelection));
+    }
+
+    private void getOrOptionsMatching(MediaType mediaType,
+                                      ServerResponse res,
+                                      Supplier> dataSupplier) {
         try {
-            Optional output = RegistryFactory.getInstance().scrape(mediaType,
-                                                                           scopeSelection,
-                                                                           nameSelection);
+            Optional output = dataSupplier.get();
+
             if (output.isPresent()) {
-                res.status(Http.Status.OK_200)
+                res.status(OK_200)
                         .headers().contentType(mediaType);
                 res.send(output.get());
             } else {
-                res.status(Http.Status.NOT_FOUND_404);
+                res.status(NOT_FOUND_404);
                 res.send();
             }
         } catch (UnsupportedOperationException ex) {
-            // The registry factory does not support that media type.
-            res.status(Http.Status.NOT_ACCEPTABLE_406);
+            // We could not find a formatter for that media type from any provider we could locate.
+            res.status(NOT_ACCEPTABLE_406);
             res.send();
+        } catch (NoClassDefFoundError ex) {
+            // Prometheus seems not to be on the path.
+            LOGGER.log(Level.DEBUG, "Unable to find Micrometer Prometheus types to scrape the registry");
+            res.status(NOT_FOUND_404);
         }
     }
 
@@ -227,6 +276,12 @@ private static MediaType bestAccepted(ServerRequest req) {
                 .orElse(null);
     }
 
+    private static MediaType bestAcceptedForMetadata(ServerRequest req) {
+        return req.headers()
+                .bestAccepted(MediaTypes.APPLICATION_JSON)
+                .orElse(null);
+    }
+
     private static KeyPerformanceIndicatorSupport.Context kpiContext(ServerRequest request) {
         return request.context()
                 .get(KeyPerformanceIndicatorSupport.Context.class)
@@ -238,7 +293,7 @@ private void setUpEndpoints(HttpRules rules) {
         // As of Helidon 4, this is the only path we should need because scope-based or metric-name-based
         // selection should use query parameters instead of paths.
         rules.get("/", this::getAll)
-                .options("/", this::rejectOptions);
+                .options("/", this::optionsAll);
 
         // routing to each scope
         // As of Helidon 4, users should use /metrics?scope=xyz instead of /metrics/xyz, and
@@ -248,15 +303,12 @@ private void setUpEndpoints(HttpRules rules) {
         Stream.of(Registry.APPLICATION_SCOPE,
                   Registry.BASE_SCOPE,
                   Registry.VENDOR_SCOPE)
-                .map(registryFactory::getRegistry)
-                .forEach(registry -> {
-                    String type = registry.scope();
-
-                    rules.get("/" + type, (req, res) -> getMatching(req, res, Set.of(type), Set.of()))
-                            .get("/" + type + "/{metric}", (req, res) -> getByName(req, res, Set.of(type))) // should use ?scope=
-                            .options("/" + type, this::rejectOptions)
-                            .options("/" + type + "/{metric}", this::rejectOptions);
-                });
+                .forEach(scope -> rules
+                        .get("/" + scope, (req, res) -> getMatching(req, res, Set.of(scope), Set.of()))
+                        .get("/" + scope + "/{metric}",
+                             (req, res) -> getByName(req, res, Set.of(scope))) // should use ?scope=
+                        .options("/" + scope, (req, res) -> optionsMatching(req, res, Set.of(scope), Set.of()))
+                        .options("/" + scope + "/{metric}", (req, res) -> optionsByName(req, res, Set.of(scope))));
     }
 
     private void getByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) {
@@ -273,16 +325,34 @@ private void postRequestProcessing(PostRequestMetricsSupport prms,
         prms.runTasks(request, response, throwable);
     }
 
-    private void rejectOptions(ServerRequest req, ServerResponse res) {
-        // Options used to return metadata but it's no longer supported unless we restore JSON support.
-        res.header(Http.HeaderNames.ALLOW, "GET");
-        res.status(Http.Status.METHOD_NOT_ALLOWED_405);
-        res.send();
+    private void optionsAll(ServerRequest req, ServerResponse res) {
+        optionsMatching(req, res, req.query().all("scope", List::of), req.query().all("name", List::of));
+    }
+
+    private void optionsByName(ServerRequest req, ServerResponse res, Iterable scopeSelection) {
+        String metricName = req.path().pathParameters().value("metric");
+        optionsMatching(req, res, scopeSelection, Set.of(metricName));
+    }
+
+    private void optionsMatching(ServerRequest req,
+                                 ServerResponse res,
+                                 Iterable scopeSelection,
+                                 Iterable nameSelection) {
+        MediaType mediaType = bestAcceptedForMetadata(req);
+        if (mediaType == null) {
+            res.header(ALLOW, "GET");
+            res.status(METHOD_NOT_ALLOWED_405);
+            res.send();
+        }
+
+        getOrOptionsMatching(mediaType, res, () -> output(mediaType,
+                                                          scopeSelection,
+                                                          nameSelection));
     }
 
     private void setUpDisabledEndpoints(HttpRules rules) {
         rules.get("/", DISABLED_ENDPOINT_HANDLER)
-                .options("/", this::rejectOptions);
+                .options("/", this::optionsAll);
 
         // routing to GET and OPTIONS for each metrics scope (registry type) and a specific metric within each scope:
         // application, base, vendor
@@ -290,7 +360,7 @@ private void setUpDisabledEndpoints(HttpRules rules) {
                 .forEach(type -> Stream.of("", "/{metric}") // for the whole scope and for a specific metric within that scope
                         .map(suffix -> "/" + type + suffix)
                         .forEach(path -> rules.get(path, DISABLED_ENDPOINT_HANDLER)
-                                .options(path, this::rejectOptions)
+                                .options(path, this::optionsAll)
                         ));
     }
 
@@ -298,8 +368,10 @@ private void setUpDisabledEndpoints(HttpRules rules) {
      * A fluent API builder to build instances of {@link MetricsFeature}.
      */
     public static final class Builder extends HelidonFeatureSupport.Builder {
-        private LazyValue registryFactory;
-        private MetricsSettings.Builder metricsSettingsBuilder = MetricsSettings.builder();
+
+        private LazyValue metricsFactory;
+        private MeterRegistry meterRegistry;
+        private MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder();
 
         private Builder() {
             super("metrics");
@@ -307,9 +379,9 @@ private Builder() {
 
         @Override
         public MetricsFeature build() {
-            if (registryFactory == null) {
-                registryFactory = LazyValue.create(() -> RegistryFactory.getInstance(metricsSettingsBuilder.build()));
-            }
+            metricsFactory = Objects.requireNonNullElseGet(metricsFactory,
+                                                           () -> LazyValue.create(() -> MetricsFactory.getInstance(
+                                                                   metricsConfigBuilder.build())));
             return new MetricsFeature(this);
         }
 
@@ -323,50 +395,32 @@ public MetricsFeature build() {
          */
         public Builder config(Config config) {
             super.config(config);
-            metricsSettingsBuilder.config(config);
+            metricsConfigBuilder.config(config);
             return this;
         }
 
         /**
-         * Assigns {@code MetricsSettings} which will be used in creating the {@code MetricsSupport} instance at build-time.
+         * Assigns {link io.helidon.metrics.api.MetricsConfig} which will be used in creating the instance at build-time.
          *
-         * @param metricsSettingsBuilder the metrics settings to assign for use in building the {@code MetricsSupport} instance
+         * @param metricsConfigBuilder the metrics config to assign for use in building the {@code MetricsSupport} instance
          * @return updated builder
          */
         @ConfiguredOption(mergeWithParent = true,
-                          type = MetricsSettings.class)
-        public Builder metricsSettings(MetricsSettings.Builder metricsSettingsBuilder) {
-            this.metricsSettingsBuilder = metricsSettingsBuilder;
+                          type = MetricsConfig.class)
+        public Builder metricsConfig(MetricsConfig.Builder metricsConfigBuilder) {
+            this.metricsConfigBuilder = metricsConfigBuilder;
             return this;
         }
 
         /**
-         * If you want to have multiple registry factories with different
-         * endpoints, you may create them using
-         * {@link RegistryFactory#create(MetricsSettings)} or
-         * {@link RegistryFactory#create()} and create multiple
-         * {@link MetricsFeature} instances with different
-         * {@link #webContext(String)} contexts}.
-         * 

- * If this method is not called, - * {@link MetricsFeature} would use the shared - * instance as provided by - * {@link io.helidon.metrics.api.RegistryFactory#getInstance(io.helidon.config.Config)} + * Assigns the {@link io.helidon.metrics.api.MeterRegistry} to query for formatting output. * - * @param factory factory to use in this metric support - * @return updated builder instance + * @param meterRegistry the meter registry to use + * @return updated builder */ - public Builder registryFactory(RegistryFactory factory) { - registryFactory = LazyValue.create(() -> factory); + public Builder meterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; return this; } - - RegistryFactory registryFactory() { - return registryFactory.get(); - } - - MetricsSettings metricsSettings() { - return metricsSettingsBuilder.build(); - } } } diff --git a/webserver/observe/metrics/src/main/java/module-info.java b/webserver/observe/metrics/src/main/java/module-info.java index b642b9dac70..ab2f9e818eb 100644 --- a/webserver/observe/metrics/src/main/java/module-info.java +++ b/webserver/observe/metrics/src/main/java/module-info.java @@ -36,7 +36,15 @@ requires io.helidon.common.context; requires io.helidon.common.features.api; + requires static micrometer.core; + requires static micrometer.registry.prometheus; + requires static simpleclient.common; + exports io.helidon.webserver.observe.metrics; provides ObserveProvider with MetricsObserveProvider; + provides io.helidon.metrics.spi.MeterRegistryFormatterProvider + with io.helidon.webserver.observe.metrics.JsonMeterRegistryFormatterProvider; + uses io.helidon.metrics.spi.MeterRegistryFormatterProvider; + } \ No newline at end of file diff --git a/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestJsonFormatting.java b/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestJsonFormatting.java new file mode 100644 index 00000000000..640473c7779 --- /dev/null +++ b/webserver/observe/metrics/src/test/java/io/helidon/webserver/observe/metrics/TestJsonFormatting.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 + * + * http://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.helidon.webserver.observe.metrics; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.helidon.common.testing.junit5.OptionalMatcher; +import io.helidon.metrics.api.Counter; +import io.helidon.metrics.api.MeterRegistry; +import io.helidon.metrics.api.Metrics; +import io.helidon.metrics.api.MetricsConfig; +import io.helidon.metrics.api.Tag; +import io.helidon.metrics.api.Timer; + +import jakarta.json.JsonObject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +class TestJsonFormatting { + private static final String SCOPE_TAG_NAME = "the-scope"; + private static MeterRegistry meterRegistry; + private static MetricsFeature feature; + + @BeforeAll + static void prep() { + MetricsConfig.Builder metricsConfigBuilder = MetricsConfig.builder() + .scopeTagName(SCOPE_TAG_NAME); + + meterRegistry = Metrics.createMeterRegistry(metricsConfigBuilder.build()); + feature = MetricsFeature.builder() + .meterRegistry(meterRegistry) + .metricsConfig(metricsConfigBuilder) + .build(); + } + + @Test + void testRetrievingAll() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c1")); + assertThat("Initial counter value", c.count(), is(0L)); + c.increment(); + assertThat("After increment", c.count(), is(1L)); + + Counter c1WithTag = meterRegistry.getOrCreate(Counter.builder("c1") + .tags(Set.of(Tag.create("t1", "v1")))); + c1WithTag.increment(4L); + + Timer d = meterRegistry.getOrCreate(Timer.builder("t1")); + d.record(3, TimeUnit.SECONDS); + + + JsonFormatter formatter = JsonFormatter.builder(meterRegistry) + .scopeTagName(SCOPE_TAG_NAME) + .build(); + + Optional result = formatter.format(); + + assertThat("Result", result, OptionalMatcher.optionalPresent()); + JsonObject app = result.get().getJsonObject("application"); + assertThat("Counter 1", + app.getJsonNumber("c1;t1=v1;the-scope=application").intValue(), + is(4)); + assertThat("Counter 2", + app.getJsonNumber("c1;the-scope=application").intValue(), + is(1)); + JsonObject timerJson = app.getJsonObject("t1"); + assertThat("Timer", timerJson, notNullValue()); + assertThat("Timer count", timerJson.getJsonNumber("count;the-scope=application").intValue(), is(1)); + } + + + @Test + void testRetrievingByName() { + Counter c = meterRegistry.getOrCreate(Counter.builder("c2")); + assertThat("Initial counter value", c.count(), is(0L)); + c.increment(); + assertThat("After increment", c.count(), is(1L)); + + Timer d = meterRegistry.getOrCreate(Timer.builder("t2")); + d.record(7, TimeUnit.SECONDS); + + JsonFormatter formatter = JsonFormatter.builder(meterRegistry) + .meterNameSelection(Set.of("c2")) + .build(); + + Optional result = formatter.format(); + assertThat("Result", result, OptionalMatcher.optionalPresent()); + + JsonObject app = result.get().getJsonObject("application"); + assertThat("Counter 2", app.getJsonNumber("c2;the-scope=application").intValue(), is(1)); + + assertThat("Timer", app.getJsonObject("t2"), nullValue()); + + } +}