diff --git a/bom/pom.xml b/bom/pom.xml
index af2bdc86589..4aadf6b13fd 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.metricshelidon-metrics-service-api
diff --git a/dependencies/pom.xml b/dependencies/pom.xml
index 2c62e745d86..ae41e1b5c41 100644
--- a/dependencies/pom.xml
+++ b/dependencies/pom.xml
@@ -98,8 +98,8 @@
1.4.02.6.22.10
- 1.11.1
- 1.11.1
+ 1.11.3
+ 1.11.33.4.33.3.04.4.0
diff --git a/metrics/api/pom.xml b/metrics/api/pom.xml
index af23d2cc263..7a70eac9be1 100644
--- a/metrics/api/pom.xml
+++ b/metrics/api/pom.xml
@@ -41,6 +41,20 @@
io.helidon.commonhelidon-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.confighelidon-config-metadata
@@ -50,13 +64,10 @@
org.eclipse.microprofile.metricsmicroprofile-metrics-api
-
- io.micrometer
- micrometer-core
- io.helidon.common.testinghelidon-common-testing-junit5
+ testorg.junit.jupiter
@@ -88,11 +99,26 @@
true
+
+ io.helidon.common.features
+ helidon-common-features-processor
+ ${helidon.version}
+ io.helidon.confighelidon-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/Bucket.java b/metrics/api/src/main/java/io/helidon/metrics/api/Bucket.java
new file mode 100644
index 00000000000..465f9948d93
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Bucket.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 boundary value and the count of observations in that 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 Bucket extends Wrapper {
+
+ /**
+ * Returns the bucket boundary.
+ *
+ * @return bucket boundary value
+ */
+ double boundary();
+
+ /**
+ * 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 boundary(TimeUnit unit);
+
+ /**
+ * Returns the number of observations in the bucket.
+ *
+ * @return observation count for the bucket
+ */
+ long count();
+}
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..12115a5bd89
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Clock.java
@@ -0,0 +1,72 @@
+/*
+ * 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 Wrapper {
+
+ /**
+ * 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 {@link 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 {@link System#nanoTime()}.
+ *
+ *
+ * @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 extends R> c) {
+ throw new UnsupportedOperationException();
+ }
+}
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..c80130ed4be
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Counter.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 monotonically increasing value.
+ */
+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);
+ }
+
+
+ /**
+ * 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(long amount);
+
+ /**
+ * Returns the cumulative count since this counter was registered.
+ *
+ * @return cumulative count since this counter was registered
+ */
+ long 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
new file mode 100644
index 00000000000..4d64dfc9055
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionStatisticsConfig.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.Optional;
+
+/**
+ * Configuration which controls the behavior of distribution statistics from meters that support them
+ * (for example, timers and distribution summaries).
+ *
+ */
+public interface DistributionStatisticsConfig extends Wrapper {
+
+ /**
+ * Creates a builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance.
+ *
+ * @return new builder
+ */
+ static Builder builder() {
+ return MetricsFactory.getInstance().distributionStatisticsConfigBuilder();
+ }
+
+ /**
+ * Returns the settings for non-aggregable percentiles.
+ *
+ * @return percentiles to compute and publish
+ */
+ Optional> percentiles();
+
+ /**
+ * 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
+ */
+ Optional maximumExpectedValue();
+
+ /**
+ * Returns the configured boundary boundaries.
+ *
+ * @return the boundary boundaries
+ */
+ Optional> buckets();
+
+ /**
+ * Builder for a new {@link io.helidon.metrics.api.DistributionStatisticsConfig} instance.
+ */
+ interface Builder extends Wrapper, io.helidon.common.Builder {
+
+ /**
+ * 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 time series percentiles.
+ *
+ * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed
+ * elsewhere.
+ *
+ *
+ * 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 time series percentiles.
+ *
+ * The system computes these percentiles locally, so they cannot be aggregated with percentiles computed
+ * elsewhere.
+ *
+ *
+ * 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 boundary boundaries.
+ *
+ * @param buckets boundary boundaries
+ * @return updated builder
+ */
+ Builder buckets(double... buckets);
+
+ /**
+ * Sets the boundary boundaries.
+ *
+ * @param buckets boundary boundaries
+ * @return updated builder
+ */
+ Builder buckets(Iterable buckets);
+ }
+}
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..a6e77f5f0fe
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/DistributionSummary.java
@@ -0,0 +1,112 @@
+/*
+ * 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 {
+
+ /**
+ * 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
+ * @return new builder
+ */
+ static Builder builder(String name,
+ DistributionStatisticsConfig.Builder configBuilder) {
+ return MetricsFactory.getInstance()
+ .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.
+ *
+ * @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();
+
+ /**
+ * 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}.
+ */
+ 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
new file mode 100644
index 00000000000..6af8e144409
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Gauge.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+/**
+ * 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 {
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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();
+
+ /**
+ * Builder for a new gauge.
+ *
+ * @param type of the state object which exposes the gauge value.
+ */
+ interface Builder extends Meter.Builder, Gauge> {
+ }
+}
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..377001f6871
--- /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 Wrapper {
+
+ /**
+ * 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 MetricsFactory.getInstance().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 pairs of percentile and the histogram value at that percentile
+ */
+ Iterable percentileValues();
+
+ /**
+ * Returns information about each of the configured buckets for the histogram.
+ *
+ * @return pairs of boundary value and count of observations in that boundary
+ */
+ Iterable 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..bb171feec38
--- /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 {
+
+ /**
+ * 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/KeyPerformanceIndicatorMetricsConfigBlueprint.java b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java
new file mode 100644
index 00000000000..effa46982ef
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/KeyPerformanceIndicatorMetricsConfigBlueprint.java
@@ -0,0 +1,87 @@
+/*
+ * 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;
+
+/**
+ * Config bean for KPI metrics configuration.
+ */
+@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;
+
+ /**
+ * 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/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java
new file mode 100644
index 00000000000..214ff56a9c5
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java
@@ -0,0 +1,155 @@
+/*
+ * 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 Wrapper {
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 extends R> 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
new file mode 100644
index 00000000000..906fb718ae0
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/MeterRegistry.java
@@ -0,0 +1,162 @@
+/*
+ * 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.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+
+/**
+ * Manages the look-up and registration of meters.
+ */
+public interface MeterRegistry extends Wrapper {
+
+ /**
+ * 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
+ */
+ 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.
+ *
+ * @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.
+ *
+ * @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
+ */
+ > M getOrCreate(B builder);
+
+ /**
+ * Locates a 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
+ */
+ default Optional getCounter(String name, Iterable tags) {
+ return get(Counter.class, name, tags);
+ }
+
+ /**
+ * Locates a 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
+ */
+ default Optional getSummary(String name, Iterable tags) {
+ return get(DistributionSummary.class, name, tags);
+ }
+
+ /**
+ * Locates a previously-registered gauge.
+ *
+ * @param name name to match
+ * @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) {
+ return get(Gauge.class, name, tags);
+ }
+
+ /**
+ * Locates a 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
+ */
+ default Optional getTimer(String name, Iterable tags) {
+ return get(Timer.class, name, tags);
+ }
+
+ /**
+ * Locates a previously-registered meter of the specified type, matching the name and tags.
+ *
+ * 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 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 get(Class mClass, String name, Iterable tags);
+
+ /**
+ * Removes a previously-registered meter.
+ *
+ * @param meter the meter to remove
+ * @return the removed meter; empty if the meter is not currently registered
+ */
+ 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; empty if the meter is not currently registered
+ */
+ 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; empty if the specified name and tags does not correspond to a registered meter
+ */
+ Optional remove(String name,
+ Iterable tags);
+}
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
new file mode 100644
index 00000000000..a1d7080284f
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Metrics.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;
+
+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
+ * 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 MetricsFactory.getInstance().globalRegistry();
+ }
+
+ /**
+ * Creates a meter registry, not added to the global registry, based on
+ * the provided 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.
+ *
+ * @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 > M getOrCreate(B builder) {
+ return globalRegistry().getOrCreate(builder);
+ }
+
+ /**
+ * Locates a 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
+ */
+ 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.
+ *
+ * @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 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.
+ *
+ * @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 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.
+ *
+ * @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 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.
+ *
+ * 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 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 Optional get(Class mClass, String name, Iterable tags) {
+ return globalRegistry().get(mClass, name, tags);
+ }
+
+ /**
+ * 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 MetricsFactory.getInstance().tagCreate(key, value);
+ }
+
+ /**
+ * Provides an {@link java.lang.Iterable} of {@link io.helidon.metrics.api.Tag} over an array of tags.
+ *
+ * @param tags tags array to convert
+ * @return iterator over the tags
+ */
+ static Iterable tags(Tag... tags) {
+ return () -> new Iterator<>() {
+
+ private int slot = 0;
+
+ @Override
+ public boolean hasNext() {
+ return slot < tags.length;
+ }
+
+ @Override
+ public Tag next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ Tag result = MetricsFactoryManager.getInstance()
+ .tagCreate(tags[slot].key(),
+ tags[slot].value());
+ slot++;
+ return result;
+ }
+ };
+ }
+
+ /**
+ * 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.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
new file mode 100644
index 00000000000..49153586d88
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigBlueprint.java
@@ -0,0 +1,179 @@
+/*
+ * 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.List;
+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;
+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;
+
+/**
+ * Blueprint for {@link io.helidon.metrics.api.MetricsConfig}.
+ */
+@ConfigBean()
+@Configured(root = true, prefix = MetricsConfigBlueprint.METRICS_CONFIG_KEY)
+@Prototype.Blueprint(decorator = MetricsConfigBlueprint.BuilderDecorator.class)
+@Prototype.CustomMethods(MetricsConfigSupport.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 = "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.
+ *
+ * @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)
+ List globalTags();
+
+ /**
+ * Application tag value added to each meter ID.
+ *
+ * @return application tag value
+ */
+ @ConfiguredOption(key = APP_TAG_CONFIG_KEY)
+ Optional appTagValue();
+
+ /**
+ * Metrics configuration node.
+ *
+ * @return metrics configuration
+ */
+ 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
+ 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);
+ }
+ }
+ }
+
+ @Prototype.FactoryMethod
+ static List createGlobalTags(Config globalTagExpression) {
+ return createGlobalTags(globalTagExpression.asString().get());
+ }
+
+ static List createGlobalTags(String pairs) {
+ // Use a TreeMap to order by tag name.
+ Map result = new TreeMap<>();
+ List allErrors = new ArrayList<>();
+ String[] assignments = pairs.split("(? errorsForThisAssignment = new ArrayList<>();
+ if (assignment.isBlank()) {
+ errorsForThisAssignment.add("empty assignment at position " + position + ": " + assignment);
+ } else {
+ // 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 (!allErrors.isEmpty()) {
+ throw new IllegalArgumentException("Error(s) in global tag expression: " + allErrors);
+ }
+ return result.values()
+ .stream()
+ .toList();
+ }
+}
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..7946db17d8a
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsConfigSupport.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.api;
+
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+import io.helidon.builder.api.Prototype;
+
+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.config()
+ .get(key)
+ .asString()
+ .asOptional();
+ }
+
+ // Pattern of a single tag assignment (tag=value):
+ // - capture reluctant match of anything
+ // - non-capturing match of an unescaped =
+ // - capture the rest.
+ static final Pattern TAG_ASSIGNMENT_PATTERN = Pattern.compile("(.*?)(?
+ * 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 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 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
+ * implementation of this interface.
+ *
+ */
+public interface MetricsFactory {
+
+ /**
+ * 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 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.
+ *
+ * @return the global meter registry
+ */
+ MeterRegistry globalRegistry();
+
+ /**
+ * 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.
+ *
+ * @return the system clock
+ */
+ Clock clockSystem();
+
+ /**
+ * 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
+ * @param configBuilder distribution stats config the summary should use
+ * @return summary builder
+ */
+ DistributionSummary.Builder distributionSummaryBuilder(String name, DistributionStatisticsConfig.Builder configBuilder);
+
+ /**
+ * 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.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 tagCreate(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/MetricsFactoryManager.java b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java
new file mode 100644
index 00000000000..f103a14acd1
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/MetricsFactoryManager.java
@@ -0,0 +1,134 @@
+/*
+ * 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.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;
+import io.helidon.common.config.GlobalConfig;
+import io.helidon.metrics.spi.MetricsFactoryProvider;
+
+/**
+ * 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(() -> {
+ 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 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/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java
new file mode 100644
index 00000000000..bd460db28ff
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java
@@ -0,0 +1,617 @@
+/*
+ * 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.time.Duration;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+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;
+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, NoOpWrapper {
+
+ 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);
+ }
+
+ 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();
+ }
+
+ String tag(String key) {
+ return tags.stream()
+ .filter(t -> t.key().equals(key))
+ .map(Tag::value)
+ .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) {
+ 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> {
+
+ 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;
+ }
+
+ abstract M build();
+
+ public B tags(Iterable tags) {
+ tags.forEach(tag -> this.tags.put(tag.key(), tag));
+ return identity();
+ }
+
+ public B tag(String key, String value) {
+ tags.put(key, Tag.create(key, value));
+ return identity();
+ }
+
+ public B description(String description) {
+ this.description = description;
+ return identity();
+ }
+
+ public B baseUnit(String unit) {
+ this.unit = unit;
+ return identity();
+ }
+
+ public B identity() {
+ return (B) this;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ 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 {
+
+ static Counter create(String name, Iterable tags) {
+ return builder(name)
+ .tags(tags)
+ .build();
+ }
+
+ static class Builder extends NoOpMeter.Builder implements io.helidon.metrics.api.Counter.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(long amount) {
+ }
+
+ @Override
+ public long count() {
+ return 0L;
+ }
+ }
+
+ static class FunctionalCounter extends Counter {
+
+ static class Builder extends Counter.Builder implements io.helidon.metrics.api.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(long amount) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ static class DistributionSummary extends NoOpMeter implements io.helidon.metrics.api.DistributionSummary {
+
+ static class Builder extends NoOpMeter.Builder
+ implements io.helidon.metrics.api.DistributionSummary.Builder {
+
+ private Builder(String name) {
+ super(name, Type.DISTRIBUTION_SUMMARY);
+ }
+
+ @Override
+ public DistributionSummary build() {
+ return new DistributionSummary(this);
+ }
+
+ @Override
+ public Builder scale(double scale) {
+ return identity();
+ }
+
+ @Override
+ public Builder distributionStatisticsConfig(
+ io.helidon.metrics.api.DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder) {
+ return identity();
+ }
+ }
+
+ 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;
+ }
+
+ @Override
+ public io.helidon.metrics.api.HistogramSnapshot snapshot() {
+ return new NoOpMeter.HistogramSnapshot(0L, 0D, 0D);
+ }
+ }
+
+ static class HistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot, NoOpWrapper {
+
+ 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) {
+ return new Builder<>(name, stateObject, fn);
+ }
+
+ static class Builder extends NoOpMeter.Builder, Gauge> implements io.helidon.metrics.api.Gauge.Builder {
+
+ 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
+ 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 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) {
+ super(name, Type.TIMER);
+ }
+
+ @Override
+ public Timer build() {
+ return new Timer(this);
+ }
+
+ @Override
+ public Builder percentiles(double... percentiles) {
+ return identity();
+ }
+
+ @Override
+ public Builder buckets(Duration... buckets) {
+ return identity();
+ }
+
+ @Override
+ public Builder minimumExpectedValue(Duration min) {
+ return identity();
+ }
+
+ @Override
+ public Builder maximumExpectedValue(Duration max) {
+ return identity();
+ }
+ }
+
+ static Builder builder(String name) {
+ return new Builder(name);
+ }
+
+ private Timer(Builder builder) {
+ super(builder);
+ }
+
+ @Override
+ public io.helidon.metrics.api.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 record(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;
+ }
+ }
+
+ static class DistributionStatisticsConfig
+ implements io.helidon.metrics.api.DistributionStatisticsConfig, NoOpWrapper {
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ static class Builder implements io.helidon.metrics.api.DistributionStatisticsConfig.Builder, NoOpWrapper {
+
+ @Override
+ public io.helidon.metrics.api.DistributionStatisticsConfig build() {
+ return new DistributionStatisticsConfig(this);
+ }
+
+ @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 buckets(double... buckets) {
+ return identity();
+ }
+
+ @Override
+ public io.helidon.metrics.api.DistributionStatisticsConfig.Builder buckets(Iterable buckets) {
+ return identity();
+ }
+ }
+
+ private DistributionStatisticsConfig(DistributionStatisticsConfig.Builder builder) {
+ }
+
+ @Override
+ public Optional> percentiles() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional minimumExpectedValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional maximumExpectedValue() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional> buckets() {
+ 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
new file mode 100644
index 00000000000..2316d2fc202
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeterRegistry.java
@@ -0,0 +1,98 @@
+/*
+ * 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.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * No-op implementation of {@link io.helidon.metrics.api.MeterRegistry}.
+ *
+ * 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, NoOpWrapper {
+
+ @Override
+ public List meters() {
+ return List.of();
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ public Optional remove(Meter.Id id) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional remove(Meter meter) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional remove(String name, Iterable tags) {
+ return remove(NoOpMeter.Id.create(name, tags));
+ }
+
+ @Override
+ public Optional get(Class mClass, String name, Iterable tags) {
+ return Optional.empty();
+ }
+
+ @Override
+ 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.empty();
+ }
+
+ 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) 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
new file mode 100644
index 00000000000..a7ec5a19884
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMetricsFactory.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+/**
+ * No-op implementation of the {@link io.helidon.metrics.api.spi.MetricFactory} interface.
+ */
+class NoOpMetricsFactory implements MetricsFactory {
+
+ private final MeterRegistry meterRegistry = new NoOpMeterRegistry();
+
+ private static final Clock SYSTEM_CLOCK = new Clock() {
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(this);
+ }
+
+ @Override
+ public long wallTime() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public long monotonicTime() {
+ return System.nanoTime();
+ }
+ };
+
+ static NoOpMetricsFactory create() {
+ return new NoOpMetricsFactory();
+ }
+
+ @Override
+ public MeterRegistry globalRegistry() {
+ return meterRegistry;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public Tag tagCreate(String key, String value) {
+ return new NoOpTag(key, value);
+ }
+
+ @Override
+ public Counter.Builder counterBuilder(String name) {
+ return NoOpMeter.Counter.builder(name);
+ }
+
+ @Override
+ public DistributionSummary.Builder distributionSummaryBuilder(String name,
+ DistributionStatisticsConfig.Builder configBuilder) {
+ return NoOpMeter.DistributionSummary.builder(name)
+ .distributionStatisticsConfig(configBuilder);
+ }
+
+ @Override
+ public Gauge.Builder gaugeBuilder(String name, T stateObject, ToDoubleFunction fn) {
+ return NoOpMeter.Gauge.builder(name, stateObject, fn);
+ }
+
+ @Override
+ public Timer.Builder timerBuilder(String name) {
+ return NoOpMeter.Timer.builder(name);
+ }
+
+ @Override
+ public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() {
+ return NoOpMeter.DistributionStatisticsConfig.builder();
+ }
+
+ @Override
+ public HistogramSnapshot histogramSnapshotEmpty(long count, double total, double max) {
+ return NoOpMeter.HistogramSnapshot.empty(count, total, max);
+ }
+
+ @Override
+ public Timer.Sample timerStart() {
+ return NoOpMeter.Timer.start();
+ }
+
+ @Override
+ public Timer.Sample timerStart(MeterRegistry registry) {
+ return NoOpMeter.Timer.start(registry);
+ }
+
+ @Override
+ public Timer.Sample timerStart(Clock clock) {
+ return NoOpMeter.Timer.start(clock);
+ }
+}
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/NoOpTag.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpTag.java
new file mode 100644
index 00000000000..96d1c6523ca
--- /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, NoOpWrapper {
+
+ static Tag create(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.create(keysAndValues[2 * slot], keysAndValues[2 * slot + 1]);
+ slot++;
+ return result;
+ }
+ };
+ }
+}
diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.java
new file mode 100644
index 00000000000..1dd1b6b4ebb
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpWrapper.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 NoOpWrapper extends Wrapper {
+
+ @Override
+ default R unwrap(Class extends R> c) {
+ return c.cast(this);
+ }
+}
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..b0e28e3e626
--- /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 Wrapper {
+
+ /**
+ * 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 create(String key, String value) {
+ return MetricsFactory.getInstance().tagCreate(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..4170f7db0af
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Timer.java
@@ -0,0 +1,226 @@
+/*
+ * 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 {
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Starts a timing sample using the default system clock.
+ *
+ * @return new sample
+ */
+ static Sample start() {
+ return MetricsFactory.getInstance().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 MetricsFactory.getInstance().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 MetricsFactory.getInstance().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 record(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);
+ }
+
+ /**
+ * Builder for a new {@link io.helidon.metrics.api.Timer}.
+ */
+ interface Builder extends Meter.Builder {
+
+ /**
+ * Sets the percentiles to compute and publish (expressing, for example, the 95th percentile as 0.95).
+ *
+ * @param percentiles percentiles to compute and publish
+ * @return updated builder
+ */
+ Builder percentiles(double... percentiles);
+
+ /**
+ * Sets the boundary boundaries.
+ *
+ * @param buckets boundary boundaries
+ * @return updated builder
+ */
+ Builder buckets(Duration... buckets);
+
+ /**
+ * 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);
+ }
+}
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..c981d4dcbbe
--- /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 Wrapper {
+
+ /**
+ * 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/Wrapper.java b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.java
new file mode 100644
index 00000000000..92310adbf4d
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Wrapper.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;
+
+/**
+ * Behavior of a type that wraps a related type, typically through delegation.
+ */
+public interface Wrapper {
+
+ /**
+ * Unwraps the delegate as the specified type.
+ *
+ * @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 extends R> c);
+}
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/io/helidon/metrics/spi/MetricsFactoryProvider.java b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.java
new file mode 100644
index 00000000000..5834f3f803b
--- /dev/null
+++ b/metrics/api/src/main/java/io/helidon/metrics/spi/MetricsFactoryProvider.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.spi;
+
+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);
+}
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..0ebcc9f61ac
--- /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;
diff --git a/metrics/api/src/main/java/module-info.java b/metrics/api/src/main/java/module-info.java
index 0f1e122e81b..9536ac1670a 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.api.MetricsFactory;
/**
* Helidon metrics API.
@@ -25,14 +26,22 @@
requires io.helidon.http;
requires transitive io.helidon.common.config;
- requires transitive microprofile.metrics.api;
+ requires io.helidon.builder.api;
requires static io.helidon.config.metadata;
- requires micrometer.core;
+
+ // TODO remove next once we no longer need MP metrics APIs
+ requires transitive microprofile.metrics.api;
+ requires io.helidon.inject.configdriven.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.spi.MetricsFactoryProvider;
+ uses io.helidon.metrics.spi.MeterRegistryFormatterProvider;
+
+ uses MetricsFactory;
}
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..0c7d8b7f7bd
--- /dev/null
+++ b/metrics/api/src/test/java/io/helidon/metrics/api/SimpleApiTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.not;
+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, OptionalMatcher.optionalEmpty());
+ fetchedCounter = Metrics.getCounter("counter2", Set.of());
+ assertThat("Fetched counter 2", fetchedCounter, OptionalMatcher.optionalEmpty());
+
+ Optional fetchedTimer = Metrics.getTimer("timer1", Metrics.tags("t1", "v1",
+ "t2", "v2"));
+ assertThat("Fetched timer", fetchedTimer, OptionalMatcher.optionalEmpty());
+ }
+}
diff --git a/metrics/api/src/test/java/io/helidon/metrics/api/TestMetricsConfigTagsHandling.java b/metrics/api/src/test/java/io/helidon/metrics/api/TestMetricsConfigTagsHandling.java
new file mode 100644
index 00000000000..b3421671469
--- /dev/null
+++ b/metrics/api/src/test/java/io/helidon/metrics/api/TestMetricsConfigTagsHandling.java
@@ -0,0 +1,83 @@
+/*
+ * 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 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.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TestMetricsConfigTagsHandling {
+
+ @Test
+ void checkSingle() {
+ var pairs = MetricsConfigBlueprint.createGlobalTags("a=4");
+ assertThat("Result", pairs, hasSize(1));
+ assertThat("Tag", pairs, hasItem(Tag.create("a", "4")));
+ }
+
+ @Test
+ void checkMultiple() {
+ var pairs = MetricsConfigBlueprint.createGlobalTags("a=11,b=12,c=13");
+ assertThat("Result", pairs, hasSize(3));
+ assertThat("Tags", pairs, allOf(hasItem(Tag.create("a", "11")),
+ hasItem(Tag.create("b", "12")),
+ hasItem(Tag.create("c", "13"))));
+ }
+
+ @Test
+ void checkQuoted() {
+ var pairs = MetricsConfigBlueprint.createGlobalTags("d=r\\=3,e=4,f=0\\,1,g=hi");
+ assertThat("Result", pairs, hasSize(4));
+ assertThat("Tags", pairs, allOf(hasItem(Tag.create("d", "r=3")),
+ hasItem(Tag.create("e", "4")),
+ hasItem(Tag.create("f", "0,1")),
+ hasItem(Tag.create("g", "hi"))));
+ }
+
+ @Test
+ void checkEmptyAssignment() {
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
+ 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"));
+ }
+}
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/pom.xml b/metrics/pom.xml
index fe55a9edab9..5532e74fbe9 100644
--- a/metrics/pom.xml
+++ b/metrics/pom.xml
@@ -31,9 +31,11 @@
metrics
+ providersprometheustrace-exemplarapiservice-api
+ testing
diff --git a/metrics/providers/micrometer/pom.xml b/metrics/providers/micrometer/pom.xml
new file mode 100644
index 00000000000..b6c11edae74
--- /dev/null
+++ b/metrics/providers/micrometer/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ 4.0.0
+
+ io.helidon.metrics
+ helidon-metrics-providers-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
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+
+
diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java
new file mode 100644
index 00000000000..8218520c1f6
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MBucket.java
@@ -0,0 +1,80 @@
+/*
+ * 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.Objects;
+import java.util.concurrent.TimeUnit;
+
+import io.helidon.metrics.api.Bucket;
+
+import io.micrometer.core.instrument.distribution.CountAtBucket;
+
+class MBucket implements Bucket {
+
+ static MBucket create(CountAtBucket delegate) {
+ return new MBucket(delegate);
+ }
+
+ private final CountAtBucket delegate;
+
+ private MBucket(CountAtBucket delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public double boundary() {
+ return delegate.bucket();
+ }
+
+ @Override
+ public double boundary(TimeUnit unit) {
+ return delegate.bucket(unit);
+ }
+
+ @Override
+ public long count() {
+ return (long) delegate.count();
+ }
+
+ @Override
+ public R unwrap(Class extends R> 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 MBucket but merely implements Bucket.
+ if (!(o instanceof Bucket that)) {
+ return false;
+ }
+ return Objects.equals(delegate.bucket(), that.boundary())
+ && Objects.equals((long) delegate.count(), that.count());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash((long) delegate.bucket(), delegate.count());
+ }
+
+ @Override
+ public String toString() {
+ return String.format("MBucket[boundary=%f,count=%d]", boundary(), count());
+ }
+}
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..01777b0addb
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MClock.java
@@ -0,0 +1,53 @@
+/*
+ * 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.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(Clock delegate) {
+ return new MClock(delegate);
+ }
+
+ private final Clock delegate;
+
+ private MClock(Clock delegate) {
+ this.delegate = delegate;
+ }
+ @Override
+ public long wallTime() {
+ return delegate.wallTime();
+ }
+
+ @Override
+ public long monotonicTime() {
+ return delegate.monotonicTime();
+ }
+
+ Clock delegate() {
+ return delegate;
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(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
new file mode 100644
index 00000000000..ed5a36212fa
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MCounter.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 io.micrometer.core.instrument.Counter;
+
+class MCounter extends MMeter implements io.helidon.metrics.api.Counter {
+
+ static Builder builder(String name) {
+ return new Builder(name);
+ }
+
+ static MCounter create(Counter counter) {
+ return new MCounter(counter);
+ }
+
+ private MCounter(Counter delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public void increment() {
+ delegate().increment();
+ }
+
+ @Override
+ public void increment(long amount) {
+ delegate().increment(amount);
+ }
+
+ @Override
+ public long count() {
+ return (long) delegate().count();
+ }
+
+ static class Builder extends MMeter.Builder
+ implements io.helidon.metrics.api.Counter.Builder {
+
+ private Builder(String 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/MDistributionStatisticsConfig.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java
new file mode 100644
index 00000000000..bfd7a6d8def
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionStatisticsConfig.java
@@ -0,0 +1,142 @@
+/*
+ * 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.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 Optional> percentiles() {
+ return Optional.ofNullable(Util.iterable(delegate.getPercentiles()));
+ }
+
+ @Override
+ public Optional minimumExpectedValue() {
+ return Optional.ofNullable(delegate.getMinimumExpectedValueAsDouble());
+ }
+
+ @Override
+ public Optional maximumExpectedValue() {
+ return Optional.ofNullable(delegate.getMaximumExpectedValueAsDouble());
+ }
+
+ @Override
+ public Optional> buckets() {
+ return Optional.ofNullable(Util.iterable(delegate.getServiceLevelObjectiveBoundaries()));
+ }
+
+ @Override
+ public R unwrap(Class extends R> 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;
+
+ private Builder() {
+ delegate = DistributionStatisticConfig.builder();
+ }
+
+ @Override
+ public MDistributionStatisticsConfig build() {
+ return new MDistributionStatisticsConfig(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 buckets(double... buckets) {
+ delegate.serviceLevelObjectives(buckets);
+ return this;
+ }
+
+ @Override
+ public Builder buckets(Iterable buckets) {
+ delegate.serviceLevelObjectives(Util.doubleArray(buckets));
+ return this;
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(delegate);
+ }
+
+ DistributionStatisticConfig.Builder delegate() {
+ return delegate;
+ }
+ }
+
+ static T chooseOpt(T fromChild, Supplier> fromParent) {
+ return Objects.requireNonNullElseGet(fromChild,
+ () -> fromParent.get().orElse(null));
+ }
+}
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..ee9786c37c2
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MDistributionSummary.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.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,
+ DistributionStatisticsConfig.Builder configBuilder) {
+ return new Builder(name, configBuilder);
+ }
+
+ static MDistributionSummary create(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();
+ }
+
+ @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, DistributionStatisticsConfig.Builder configBuilder) {
+ this(name, configBuilder.build());
+ }
+ private Builder(String name, DistributionStatisticsConfig config) {
+ super(DistributionSummary.builder(name)
+ .publishPercentiles(config.percentiles()
+ .map(Util::doubleArray)
+ .orElse(DEFAULT.getPercentiles()))
+ .serviceLevelObjectives(config.buckets()
+ .map(Util::doubleArray)
+ .orElse(DEFAULT.getServiceLevelObjectiveBoundaries()))
+ .minimumExpectedValue(config.minimumExpectedValue()
+ .orElse(DEFAULT.getMinimumExpectedValueAsDouble()))
+ .maximumExpectedValue(config.maximumExpectedValue()
+ .orElse(DEFAULT.getMaximumExpectedValueAsDouble())));
+ prep(delegate()::tags,
+ delegate()::description,
+ delegate()::baseUnit);
+ }
+
+ @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.buckets().ifPresent(slos -> delegate.serviceLevelObjectives(Util.doubleArray(slos)));
+ config.minimumExpectedValue().ifPresent(delegate::minimumExpectedValue);
+ config.maximumExpectedValue().ifPresent(delegate::maximumExpectedValue);
+
+ return identity();
+ }
+ }
+}
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..56e3c04f561
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MGauge.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.micrometer;
+
+import java.util.function.ToDoubleFunction;
+
+import io.micrometer.core.instrument.Gauge;
+
+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 create(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(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/MHistogramSnapshot.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java
new file mode 100644
index 00000000000..8dea154364e
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MHistogramSnapshot.java
@@ -0,0 +1,125 @@
+/*
+ * 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.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;
+
+class MHistogramSnapshot implements io.helidon.metrics.api.HistogramSnapshot {
+
+ static MHistogramSnapshot create(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.create(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 Bucket next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ return MBucket.create(counts[slot++]);
+ }
+ };
+ }
+
+ @Override
+ public void outputSummary(PrintStream out, double scale) {
+ delegate.outputSummary(out, scale);
+
+ }
+
+ @Override
+ public R unwrap(Class extends R> 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
new file mode 100644
index 00000000000..54753baa225
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeter.java
@@ -0,0 +1,188 @@
+/*
+ * 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.Objects;
+import java.util.function.Function;
+
+import io.micrometer.core.instrument.Meter;
+import io.micrometer.core.instrument.Tag;
+
+/**
+ * Adapter to Micrometer meter for Helidon metrics.
+ */
+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 id;
+ }
+
+ @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());
+ }
+
+ @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);
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(delegate);
+ }
+
+ protected M delegate() {
+ return delegate;
+ }
+
+ abstract static class Builder, HM extends io.helidon.metrics.api.Meter> {
+
+ private final B delegate;
+ private Function, B> tagsSetter;
+ private Function descriptionSetter;
+ private Function baseUnitSetter;
+
+ protected Builder(B delegate) {
+ 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;
+ }
+
+ public HB tags(Iterable tags) {
+ tagsSetter.apply(MTag.tags(tags));
+ return identity();
+ }
+
+ public HB description(String description) {
+ descriptionSetter.apply(description);
+ return identity();
+ }
+
+ public HB baseUnit(String baseUnit) {
+ baseUnitSetter.apply(baseUnit);
+ return identity();
+ }
+
+ public HB identity() {
+ return (HB) this;
+ }
+
+// abstract HM register(MMeterRegistry meterRegistry);
+
+ }
+
+ 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.create(iter.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
new file mode 100644
index 00000000000..5fb90ccba25
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MMeterRegistry.java
@@ -0,0 +1,371 @@
+/*
+ * 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.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;
+import io.helidon.metrics.api.MetricsConfig;
+
+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;
+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.PrometheusMeterRegistry;
+
+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) {
+ // 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();
+ 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 final Map scopes = new ConcurrentHashMap<>();
+ private final ReentrantLock lock = new ReentrantLock();
+ private final String scopeTagName;
+
+ private MMeterRegistry(MeterRegistry delegate,
+ Clock clock,
+ MetricsConfig metricsConfig) {
+ this.delegate = delegate;
+ this.clock = clock;
+ delegate.config()
+ .onMeterAdded(this::recordAdd)
+ .onMeterRemoved(this::recordRemove);
+ List globalTags = metricsConfig.globalTags();
+ 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() {
+ return meters.values().stream().toList();
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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.
+
+ // 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) {
+ Counter counter = cBuilder.delegate().register(delegate);
+ meter = counter;
+ } else if (builder instanceof MDistributionSummary.Builder sBuilder) {
+ DistributionSummary summary = sBuilder.delegate().register(delegate);
+ meter = summary;
+ } else if (builder instanceof MGauge.Builder> gBuilder) {
+ Gauge gauge = gBuilder.delegate().register(delegate);
+ meter = gauge;
+ } else if (builder instanceof MTimer.Builder tBuilder) {
+ 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(),
+ List.of(MCounter.Builder.class.getName(),
+ MDistributionSummary.Builder.class.getName(),
+ MGauge.Builder.class.getName(),
+ MTimer.Builder.class.getName())));
+ }
+ return (HM) meters.get(meter);
+ }
+
+ @Override
+ public Optional get(Class mClass,
+ String name,
+ Iterable tags) {
+
+ Search search = delegate().find(name)
+ .tags(Util.tags(tags));
+ Meter match = search.meter();
+
+ 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 Optional remove(io.helidon.metrics.api.Meter meter) {
+ return remove(meter.id());
+ }
+
+ @Override
+ public Optional remove(io.helidon.metrics.api.Meter.Id id) {
+ return internalRemove(id.name(), Util.tags(id.tags()));
+ }
+
+ @Override
+ public Optional remove(String name,
+ Iterable tags) {
+ return internalRemove(name, Util.tags(tags));
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(delegate);
+ }
+
+ MeterRegistry delegate() {
+ return delegate;
+ }
+
+ private Optional internalRemove(String name,
+ Iterable tags) {
+ Meter nativeMeter = delegate.find(name)
+ .tags(tags)
+ .meter();
+ if (nativeMeter != null) {
+ io.helidon.metrics.api.Meter result = meters.get(nativeMeter);
+ delegate.remove(nativeMeter);
+ return Optional.of(result);
+ }
+ return Optional.empty();
+ }
+
+ private void recordAdd(Meter addedMeter) {
+ 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) {
+ 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();
+ }
+ }
+
+ /**
+ * 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/MTag.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java
new file mode 100644
index 00000000000..f4492a721f9
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTag.java
@@ -0,0 +1,130 @@
+/*
+ * 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.Objects;
+import java.util.StringJoiner;
+
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+
+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.create(iter.next());
+ }
+ };
+ }
+
+ static Tags mTags(Iterable tags) {
+ return Tags.of(tags(tags));
+ }
+
+ 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 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) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public String key() {
+ return delegate.getKey();
+ }
+
+ @Override
+ 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());
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ return c.cast(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
new file mode 100644
index 00000000000..6c6ca8fdf51
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MTimer.java
@@ -0,0 +1,199 @@
+/*
+ * 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.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import io.helidon.metrics.api.HistogramSnapshot;
+
+import io.micrometer.core.instrument.Clock;
+import io.micrometer.core.instrument.Timer;
+
+class MTimer extends MMeter implements io.helidon.metrics.api.Timer {
+
+ 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 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) {
+ // 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 {
+
+ 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);
+ }
+
+ @Override
+ public HistogramSnapshot takeSnapshot() {
+ return MHistogramSnapshot.create(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);
+ }
+
+ @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 {
+
+ private Builder(String name) {
+ super(Timer.builder(name));
+ prep(delegate()::tags,
+ delegate()::description,
+ baseUnit -> null);
+ }
+
+ @Override
+ public Builder percentiles(double... percentiles) {
+ delegate().publishPercentiles(percentiles);
+ return identity();
+ }
+
+ @Override
+ public Builder buckets(Duration... buckets) {
+ delegate().serviceLevelObjectives(buckets);
+ return identity();
+ }
+
+ @Override
+ public Builder minimumExpectedValue(Duration min) {
+ delegate().minimumExpectedValue(min);
+ return identity();
+ }
+
+ @Override
+ public Builder maximumExpectedValue(Duration max) {
+ delegate().maximumExpectedValue(max);
+ return identity();
+ }
+ }
+}
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..9259615b6e4
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MValueAtPercentile.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 java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import io.micrometer.core.instrument.distribution.ValueAtPercentile;
+
+class MValueAtPercentile implements io.helidon.metrics.api.ValueAtPercentile {
+
+ static MValueAtPercentile create(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);
+ }
+
+ @Override
+ public R unwrap(Class extends R> 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/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.java
new file mode 100644
index 00000000000..26b859487fb
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactory.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.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;
+import io.helidon.metrics.api.DistributionSummary;
+import io.helidon.metrics.api.Gauge;
+import io.helidon.metrics.api.HistogramSnapshot;
+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;
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import io.micrometer.prometheus.PrometheusMeterRegistry;
+
+/**
+ * Implementation of the neutral Helidon metrics factory based on Micrometer.
+ */
+class MicrometerMetricsFactory implements MetricsFactory {
+
+ static MicrometerMetricsFactory create(MetricsConfig metricsConfig) {
+ return new MicrometerMetricsFactory(metricsConfig);
+ }
+
+ private final LazyValue globalMeterRegistry;
+
+ private MicrometerMetricsFactory(MetricsConfig metricsConfig) {
+ globalMeterRegistry = LazyValue.create(() -> {
+ ensurePrometheusRegistry(Metrics.globalRegistry, metricsConfig);
+ return MMeterRegistry.create(Metrics.globalRegistry, metricsConfig);
+ });
+ }
+
+ @Override
+ 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();
+ }
+
+ private static void ensurePrometheusRegistry(CompositeMeterRegistry compositeMeterRegistry,
+ MetricsConfig metricsConfig) {
+ if (compositeMeterRegistry
+ .getRegistries()
+ .stream()
+ .noneMatch(mr -> mr instanceof PrometheusMeterRegistry)) {
+ compositeMeterRegistry.add(new PrometheusMeterRegistry(key -> metricsConfig.lookupConfig(key).orElse(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
+ public R unwrap(Class extends R> c) {
+ return c.cast(delegate);
+ }
+ };
+ }
+
+ @Override
+ public Counter.Builder counterBuilder(String name) {
+ return MCounter.builder(name);
+ }
+
+ @Override
+ public DistributionStatisticsConfig.Builder distributionStatisticsConfigBuilder() {
+ return MDistributionStatisticsConfig.builder();
+ }
+
+ @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 Timer.Builder timerBuilder(String name) {
+ return MTimer.builder(name);
+ }
+
+ @Override
+ public Timer.Sample timerStart() {
+ return MTimer.start();
+ }
+
+ @Override
+ public Timer.Sample timerStart(MeterRegistry registry) {
+ return MTimer.start(registry);
+ }
+
+ @Override
+ public Timer.Sample timerStart(Clock clock) {
+ return MTimer.start(clock);
+ }
+
+ @Override
+ public Tag tagCreate(String key, String value) {
+ return MTag.of(key, value);
+ }
+
+ @Override
+ 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/MicrometerMetricsFactoryProvider.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java
new file mode 100644
index 00000000000..80ef16bf700
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/MicrometerMetricsFactoryProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.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;
+
+/**
+ * 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 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;
+// }
+// }
+
+ @Override
+ 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;
+ }
+}
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/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..b0f7773bbba
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/micrometer/Util.java
@@ -0,0 +1,108 @@
+/*
+ * 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;
+
+import io.micrometer.core.instrument.Tag;
+
+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 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/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
new file mode 100644
index 00000000000..09353081fc0
--- /dev/null
+++ b/metrics/providers/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;
diff --git a/metrics/providers/micrometer/src/main/java/module-info.java b/metrics/providers/micrometer/src/main/java/module-info.java
new file mode 100644
index 00000000000..2674ff232bf
--- /dev/null
+++ b/metrics/providers/micrometer/src/main/java/module-info.java
@@ -0,0 +1,35 @@
+/*
+ * 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.MicrometerMetricsFactoryProvider;
+import io.helidon.metrics.micrometer.MicrometerPrometheusFormatterProvider;
+
+/**
+ * Micrometer adapter for Helidon metrics API.
+ */
+module io.helidon.metrics.micrometer {
+
+ requires io.helidon.metrics.api;
+ requires micrometer.core;
+ 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/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.java
new file mode 100644
index 00000000000..0cbf66e7aa8
--- /dev/null
+++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/micrometer/TestCounter.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.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 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(0L));
+ mCounter.increment();
+ assertThat("Updated value", c.count(), is(1L));
+ }
+}
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/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/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/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/providers/pom.xml b/metrics/providers/pom.xml
new file mode 100644
index 00000000000..2905bb3a418
--- /dev/null
+++ b/metrics/providers/pom.xml
@@ -0,0 +1,62 @@
+
+
+
+
+ 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
+
+
+
+
+
+ io.helidon.metrics
+ helidon-metrics-testing
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ io.helidon.metrics:helidon-metrics-testing
+
+
+
+
+
+
+
diff --git a/metrics/service-api/src/main/java/module-info.java b/metrics/service-api/src/main/java/module-info.java
index 7f89a7a5996..abe1941e6bb 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.
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/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java
new file mode 100644
index 00000000000..eb6b92077d7
--- /dev/null
+++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/SimpleMeterRegistryTests.java
@@ -0,0 +1,71 @@
+/*
+ * 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.List;
+
+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.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class SimpleMeterRegistryTests {
+
+ private static MeterRegistry meterRegistry;
+
+ @BeforeAll
+ static void prep() {
+ meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create());
+ }
+
+ @Test
+ void testConflictingMetadata() {
+ meterRegistry.getOrCreate(Counter.builder("b"));
+
+ assertThrows(IllegalArgumentException.class, () ->
+ meterRegistry.getOrCreate(Timer.builder("b")));
+ }
+
+ @Test
+ void testSameNameNoTags() {
+ 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)));
+ }
+
+ @Test
+ void testSameNameSameTwoTags() {
+ var tags = List.of(Tag.create("foo", "1"),
+ Tag.create("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)));
+ }
+}
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..15cab606176
--- /dev/null
+++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestCounter.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.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(0L));
+ c.increment();
+ assertThat("After increment", c.count(), is(1L));
+ }
+
+ @Test
+ void incrWithValue() {
+ Counter c = meterRegistry.getOrCreate(Counter.builder("c2"));
+ 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(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(initialValue));
+
+ cAgain.increment(incr);
+ assertThat("Value after second update", cAgain.count(), is(initialValue + incr));
+ }
+}
diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDeletions.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDeletions.java
new file mode 100644
index 00000000000..359b231f2d2
--- /dev/null
+++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/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.testing;
+
+
+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/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java
new file mode 100644
index 00000000000..4569c8088ee
--- /dev/null
+++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestDistributionSummary.java
@@ -0,0 +1,155 @@
+/*
+ * 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.List;
+import java.util.concurrent.TimeUnit;
+
+import io.helidon.metrics.api.Bucket;
+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 {
+
+ private static MeterRegistry meterRegistry;
+
+ @BeforeAll
+ static void prep() {
+ meterRegistry = Metrics.createMeterRegistry(MetricsConfig.create());
+ }
+
+ 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",
+ DistributionStatisticsConfig.builder());
+ 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 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));
+ assertThat("Snapshot total as time (microseconds)", snapshot.total(TimeUnit.MICROSECONDS), is(0.016));
+ }
+
+ @Test
+ void testPercentiles() {
+ 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)),
+ equalTo(Cab.create(10.0D, 4)),
+ equalTo(Cab.create(15.0D, 4))));
+
+ }
+
+ 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 extends R> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Vap[percentile=%f,value=%f]", percentile, value);
+ }
+ }
+
+ 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 boundary(TimeUnit unit) {
+ return unit.convert((long) boundary, TimeUnit.NANOSECONDS);
+ }
+
+ @Override
+ public R unwrap(Class extends R> c) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Vap[boundary=%f,count=%d]", boundary, count);
+ }
+ }
+}
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;
+ }
+ }
+}
diff --git a/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGlobalTags.java b/metrics/testing/src/main/java/io/helidon/metrics/testing/TestGlobalTags.java
new file mode 100644
index 00000000000..ae08679f273
--- /dev/null
+++ b/metrics/testing/src/main/java/io/helidon/metrics/testing/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.testing;
+
+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")));
+
+ }
+}
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