Repository: helidon-io/helidon (java)

Issue:
Expose `unwrap` on Helidon metrics API builder types
## Environment Details
* Helidon Version: 4.x
* Helidon SE or Helidon MP
* JDK version:
* OS:
* Docker version (if applicable):

----------

## Problem Description
The Helidon neutral metrics API exposes an abstraction of metrics behavior, similar to but not identical to Micrometer's approach.

We did not intend to include every feature of Micrometer in the Helidon API but by implementing`unwrap` allow developers relatively easy access to Micrometer features (or features of other potential implementations) not included in our neutral API.

The main level types (for example `Counter` and `Timer`) have `unwrap` but the builder types _do not_ and it would be helpful if they did. 


Pull request:
Add `unwrap` support to metrics builders
### Description
Resolves #8586 

The builders of the various meter types now support `unwrap`. 

The Micrometer-based implementations all have delegates to the corresponding Micrometer builders, so the builder `unwrap` method simply casts `delegate` to the caller-specified type.

The no-op builder implementation casts `this` since the no-op builders have no delegate builders.

The PR also includes minor updates to tests to invoke the builders' `unwrap` method and invoke a method on the underlying implementation's builder.

### Documentation
Doc update included in the PR.

Patch:
diff --git a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
index 8813811400..16d3b1e85c 100644
--- a/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
+++ b/docs/src/main/asciidoc/mp/config/advanced-configuration.adoc
@@ -149,7 +149,7 @@ sources:
 <5> Loads the system properties config source.
 <6> Loads a properties file
 <7> Location of the file: `/conf/prod.properties` on the file system
-<8> Custom ordinal, if not defined, the value defined in the file, or default value is used. The source precedence order is the order of appearance in the file.
+<8> Custom ordinal, if not defined, the value defined in the file, or default value is used. The source precedence order is the order of appearance in the file. The default is 100.
 <9> The file is optional (if not optional and no file is found, the bootstrap fails)
 <10> Loads a YAML file
 <11> Location of the file: `META-INF/database.yaml` on the classpath
diff --git a/docs/src/main/asciidoc/mp/config/introduction.adoc b/docs/src/main/asciidoc/mp/config/introduction.adoc
index e3b1cf3b72..3141573b22 100644
--- a/docs/src/main/asciidoc/mp/config/introduction.adoc
+++ b/docs/src/main/asciidoc/mp/config/introduction.adoc
@@ -103,7 +103,10 @@ In order to properly configure your application using configuration sources, you
 Each Config Source has an ordinal that determines the priority of the Config Source.
 A Config Source with higher ordinal has higher priority as compared to the Config Source with
 lower ordinal. The values taken from the high-priority Config Source overrides the values
-from low-priority Config Source.
+from low-priority Config Source. The default value is 100.
+
+NOTE: In MP, the ordering is not defined for sources that have the same ordinal. 
+
 
 This helps to customize the configuration of Config Sources using external Config Source
 if an external Config Source has higher ordinal values than the built-in Config Sources of the application.
diff --git a/docs/src/main/asciidoc/se/health.adoc b/docs/src/main/asciidoc/se/health.adoc
index b999f91a5b..69c384549b 100644
--- a/docs/src/main/asciidoc/se/health.adoc
+++ b/docs/src/main/asciidoc/se/health.adoc
@@ -492,9 +492,9 @@ specification is also provided for the Kubernetes service and deployment.
 ----
 include::{sourcedir}/se/HealthSnippets.java[tag=snippet_8, indent=0]
 ----
-<1> The health service for the `liveness` probe is exposed at `/observe/health/live`.
+<1> The health service for the `liveness` probe is exposed at `/health/live`.
 <2> Using the built-in health checks for the `liveness` probe.
-<3> The health service for the `readiness` probe is exposed at `/observe/health/ready`.
+<3> The health service for the `readiness` probe is exposed at `/health/ready`.
 <4> Using a custom health check for a pseudo database that is always `UP`.
 <5> Route the `observe` feature exclusively on the `observe` socket.
 <6> The default socket uses port 8080 for the default routes.
diff --git a/docs/src/main/asciidoc/se/metrics/metrics.adoc b/docs/src/main/asciidoc/se/metrics/metrics.adoc
index 9c8de809c1..be776226fd 100644
--- a/docs/src/main/asciidoc/se/metrics/metrics.adoc
+++ b/docs/src/main/asciidoc/se/metrics/metrics.adoc
@@ -180,6 +180,15 @@ Your code can then operate on the returned meter as needed to record new measure
 
 The example code in the <<Examples>> section below illustrates how to register, retrieve, and update meters.
 
+=== Accessing the Underlying Implementation: `unwrap`
+The neutral Helidon metrics API is an abstraction of common metrics behavior independent from any given implementation. As such, we intentionally excluded some implementation-specific behavior from the API.
+
+Sometimes you might want access to methods that are present in a particular metrics implementation but not in the Helidon API. Helidon allows that via the `unwrap` method on the meter types and on their builders. Each full implementation of the Helidon meter types and their builders refers to a delegate meter or delegate builder internally. The `unwrap` method lets you obtain the delegate, cast to the type you want.
+
+Of course, using this technique binds your code to a particular metrics implementation.
+
+The link:{metrics-javadoc-base-url}/io/helidon/metrics/api/Wrapper.html[`Wrapper`] interface declares the `unwrap` method which accepts a class parameter to which the delegate is cast. You can then invoke any method declared on the implementation-specific type.
+
 // Here's Configuration.
 include::{rootdir}/includes/metrics/metrics-config.adoc[tag=config-intro]
 
diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java
index 18ed071180..e74d83a416 100644
--- a/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/Meter.java
@@ -116,7 +116,7 @@ public interface Meter extends Wrapper {
      * @param <B> type of the builder
      * @param <M> type of the meter the builder creates
      */
-    interface Builder<B extends Builder<B, M>, M extends Meter> {
+    interface Builder<B extends Builder<B, M>, M extends Meter> extends Wrapper {
 
         /**
          * Returns the type-correct "this".
@@ -202,6 +202,8 @@ public interface Meter extends Wrapper {
          * @return the assigned scope if set; empty otherwise
          */
         Optional<String> scope();
+
+
     }
 
     /**
diff --git a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java
index 274efff036..f0495bd0c0 100644
--- a/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java
+++ b/metrics/api/src/main/java/io/helidon/metrics/api/NoOpMeter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -151,7 +151,7 @@ class NoOpMeter implements Meter, NoOpWrapper {
         }
     }
 
-    abstract static class Builder<B extends Builder<B, M>, M extends Meter> {
+    abstract static class Builder<B extends Builder<B, M>, M extends Meter> implements Wrapper {
 
         private final String name;
         private final Map<String, String> tags = new TreeMap<>(); // tree map for ordering by tag name
@@ -215,6 +215,11 @@ class NoOpMeter implements Meter, NoOpWrapper {
         public Optional<String> scope() {
             return Optional.ofNullable(scope);
         }
+
+        @Override
+        public <R> R unwrap(Class<? extends R> c) {
+            return c.cast(this);
+        }
     }
 
     static class Counter extends NoOpMeter implements io.helidon.metrics.api.Counter {
diff --git a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeter.java b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeter.java
index 2146d1f9fd..9d4b94d784 100644
--- a/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeter.java
+++ b/metrics/providers/micrometer/src/main/java/io/helidon/metrics/providers/micrometer/MMeter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -25,6 +25,7 @@ import java.util.TreeMap;
 
 import io.helidon.metrics.api.Meter;
 import io.helidon.metrics.api.Tag;
+import io.helidon.metrics.api.Wrapper;
 
 import io.micrometer.core.instrument.Timer;
 
@@ -164,7 +165,8 @@ class MMeter<M extends io.micrometer.core.instrument.Meter> implements Meter {
     abstract static class Builder<B,
             M extends io.micrometer.core.instrument.Meter,
             HB extends Builder<B, M, HB, HM>,
-            HM extends MMeter<M>> {
+            HM extends MMeter<M>>
+            implements Wrapper {
 
         private final String name;
         private final B delegate;
@@ -248,6 +250,11 @@ class MMeter<M extends io.micrometer.core.instrument.Meter> implements Meter {
             return new TreeMap<>(tags);
         }
 
+        @Override
+        public <R> R unwrap(Class<? extends R> c) {
+            return c.cast(delegate);
+        }
+
         protected B delegate() {
             return delegate;
         }


Test patch:
diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java
index 4250fad825..f1f0152df3 100644
--- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java
+++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestCounter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -17,6 +17,7 @@ package io.helidon.metrics.providers.micrometer;
 
 import java.util.List;
 
+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;
@@ -40,10 +41,14 @@ class TestCounter {
 
     @Test
     void testUnwrap() {
-        Counter c = meterRegistry.getOrCreate(Counter.builder("c4"));
+        Counter.Builder builder = Counter.builder("c4");
+        String descr = "Example counter";
+        builder.unwrap(io.micrometer.core.instrument.Counter.Builder.class).description(descr);
+        Counter c = meterRegistry.getOrCreate(builder);
         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));
+        assertThat("Description", c.description(), OptionalMatcher.optionalValue(is(descr)));
     }
 }
diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestDistributionSummary.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestDistributionSummary.java
index 46f1915a5a..fdecfcc360 100644
--- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestDistributionSummary.java
+++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestDistributionSummary.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -15,12 +15,12 @@
  */
 package io.helidon.metrics.providers.micrometer;
 
+import java.time.Duration;
 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;
@@ -39,12 +39,14 @@ class TestDistributionSummary {
 
     @Test
     void testUnwrap() {
-        DistributionSummary summary = meterRegistry.getOrCreate(DistributionSummary.builder("a"));
+        DistributionSummary.Builder builder = DistributionSummary.builder("a");
+        builder.unwrap(io.micrometer.core.instrument.DistributionSummary.Builder.class)
+                .distributionStatisticExpiry(Duration.ofMinutes(10));
+        DistributionSummary summary = meterRegistry.getOrCreate(builder);
         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));
diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java
index f8bbf61807..83be62a210 100644
--- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java
+++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestGauge.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -42,9 +42,9 @@ class TestGauge {
         long initialValue = 4L;
         long incr = 2L;
         AtomicLong value = new AtomicLong(initialValue);
-        Gauge g = meterRegistry.getOrCreate(Gauge.builder("a",
-                                                          value,
-                                                          v -> (double) v.get()));
+        Gauge.Builder<Double> builder = Gauge.builder("a", value, v -> (double) v.get());
+        builder.unwrap(io.micrometer.core.instrument.Gauge.Builder.class).strongReference(true);
+        Gauge<Double> g = meterRegistry.getOrCreate(builder);
 
         io.micrometer.core.instrument.Gauge mGauge = g.unwrap(io.micrometer.core.instrument.Gauge.class);
         assertThat("Initial value", mGauge.value(), is((double) initialValue));
diff --git a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java
index 41a68bb262..4b32aa3a45 100644
--- a/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java
+++ b/metrics/providers/micrometer/src/test/java/io/helidon/metrics/providers/micrometer/TestTimer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2023, 2024 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.
@@ -15,6 +15,7 @@
  */
 package io.helidon.metrics.providers.micrometer;
 
+import java.time.Duration;
 import java.util.concurrent.TimeUnit;
 
 import io.helidon.metrics.api.MeterRegistry;
@@ -43,7 +44,11 @@ class TestTimer {
         long initialValue = 0L;
         long incrA = 2L;
         long incrB = 7L;
-        Timer t = meterRegistry.getOrCreate(Timer.builder("a"));
+
+        Timer.Builder builder = Timer.builder("a");
+        io.micrometer.core.instrument.Timer.Builder mBuilder = builder.unwrap(io.micrometer.core.instrument.Timer.Builder.class);
+        mBuilder.distributionStatisticExpiry(Duration.ofMinutes(10));
+        Timer t = meterRegistry.getOrCreate(builder);
 
         io.micrometer.core.instrument.Timer mTimer = t.unwrap(io.micrometer.core.instrument.Timer.class);
         assertThat("Initial value", mTimer.count(), is(initialValue));