> restClientsDiscovered = new HashSet<>();
+
+ private Listener() {
+ restClientMetricsFilter = RestClientMetricsFilter.create();
+ }
+
+ private void onNewClient(Class> serviceInterface, RestClientBuilder builder) {
+ if (restClientMetricsConfig.get().enabled()) {
+ // Users might build multiple REST client builders (and instances) for a given interface, but we
+ // register metrics (and create metric-related work for the filter to do) only upon first
+ // discovering a given service interface.
+ if (restClientsDiscovered.add(serviceInterface)) {
+ ext.get().registerMetricsForRestClient(serviceInterface);
+ }
+ builder.register(restClientMetricsFilter, Priorities.USER - 100);
+ }
+ }
+ }
+}
diff --git a/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsConfigBlueprint.java b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsConfigBlueprint.java
new file mode 100644
index 00000000000..3991d9b4bf6
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsConfigBlueprint.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import io.helidon.builder.api.Option;
+import io.helidon.builder.api.Prototype;
+
+/**
+ * Configuration settings for MP REST client metrics.
+ */
+@Prototype.Blueprint
+@Prototype.Configured(RestClientMetricsConfigBlueprint.CONFIG_KEY)
+interface RestClientMetricsConfigBlueprint {
+
+ /**
+ * Root=level config key for REST client metrics settings.
+ */
+ String CONFIG_KEY = "rest-client.metrics";
+
+ /**
+ * Whether REST client metrics functionality is enabled.
+ *
+ * @return if REST client metrics are configured to be enabled
+ */
+ @Option.Configured
+ @Option.DefaultBoolean(true)
+ boolean enabled();
+}
diff --git a/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsFilter.java b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsFilter.java
new file mode 100644
index 00000000000..451f6455b36
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/RestClientMetricsFilter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import java.lang.reflect.Method;
+
+import jakarta.annotation.Priority;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.ws.rs.Priorities;
+import jakarta.ws.rs.client.ClientRequestContext;
+import jakarta.ws.rs.client.ClientRequestFilter;
+import jakarta.ws.rs.client.ClientResponseContext;
+import jakarta.ws.rs.client.ClientResponseFilter;
+import jakarta.ws.rs.ext.Provider;
+
+/**
+ * Filter which automatically registers and updates metrics for outgoing REST client requests.
+ *
+ * An instance of this filter is added explicitly to the filter chain for each REST client interface
+ *
+ */
+@Priority(Priorities.USER - 100)
+@Provider
+class RestClientMetricsFilter implements ClientRequestFilter, ClientResponseFilter {
+
+ static final String REST_CLIENT_METRICS_CONFIG_KEY = "rest-client.metrics";
+
+ private static final String INVOKED_METHOD = "org.eclipse.microprofile.rest.client.invokedMethod";
+
+ private final RestClientMetricsCdiExtension ext;
+
+ private RestClientMetricsFilter() {
+ ext = CDI.current().getBeanManager().getExtension(RestClientMetricsCdiExtension.class);
+ }
+
+ static RestClientMetricsFilter create() {
+ return new RestClientMetricsFilter();
+ }
+
+ @Override
+ public void filter(ClientRequestContext requestContext) {
+ Method javaMethod = (Method) requestContext.getProperty(INVOKED_METHOD);
+ if (javaMethod != null) {
+ ext.doPreWork(javaMethod, requestContext);
+ }
+ }
+
+ @Override
+ public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) {
+ Method javaMethod = (Method) requestContext.getProperty(INVOKED_METHOD);
+ if (javaMethod != null) {
+ ext.doPostWork(javaMethod, requestContext);
+ }
+ }
+}
diff --git a/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/package-info.java b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/package-info.java
new file mode 100644
index 00000000000..1e6f33056d4
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/main/java/io/helidon/microprofile/restclientmetrics/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 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.
+ * 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.
+ */
+/**
+ * Metrics support for MP REST Client.
+ */
+package io.helidon.microprofile.restclientmetrics;
diff --git a/microprofile/rest-client-metrics/src/main/java/module-info.java b/microprofile/rest-client-metrics/src/main/java/module-info.java
new file mode 100644
index 00000000000..0d22d561959
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/main/java/module-info.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 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.
+ * 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.common.features.api.Feature;
+import io.helidon.common.features.api.HelidonFlavor;
+
+/**
+ * MP Rest client metrics.
+ *
+ * @see org.eclipse.microprofile.rest.client
+ */
+@Feature(value = "REST Client Metrics",
+ description = "MicroProfile REST client spec implementation",
+ in = HelidonFlavor.MP,
+ path = "REST Client Metrics"
+)
+@SuppressWarnings({"requires-automatic", "requires-transitive-automatic"})
+module io.helidon.microprofile.restclient.metrics {
+
+ requires io.helidon.microprofile.metrics;
+
+ requires transitive jakarta.ws.rs;
+ requires jakarta.inject;
+ requires transitive jersey.common;
+ requires microprofile.metrics.api;
+ requires jakarta.cdi;
+ requires io.helidon.metrics.api;
+ requires microprofile.rest.client.api;
+ requires io.helidon.webserver;
+ requires java.xml;
+
+ requires static io.helidon.common.features.api;
+
+ exports io.helidon.microprofile.restclientmetrics;
+
+ opens io.helidon.microprofile.restclientmetrics to weld.core.impl;
+
+ provides jakarta.enterprise.inject.spi.Extension
+ with io.helidon.microprofile.restclientmetrics.RestClientMetricsCdiExtension;
+
+ provides org.glassfish.jersey.internal.spi.AutoDiscoverable
+ with io.helidon.microprofile.restclientmetrics.RestClientMetricsAutoDiscoverable;
+
+ provides org.eclipse.microprofile.rest.client.spi.RestClientListener
+ with io.helidon.microprofile.restclientmetrics.RestClientMetricsClientListener;
+}
\ No newline at end of file
diff --git a/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/MetricIdMatcher.java b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/MetricIdMatcher.java
new file mode 100644
index 00000000000..0975ac69087
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/MetricIdMatcher.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import org.eclipse.microprofile.metrics.MetricID;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+class MetricIdMatcher {
+
+ static WithName withName(Matcher super String> matcher) {
+ return new WithName(matcher);
+ }
+
+ private static class WithName extends TypeSafeMatcher {
+
+ private final Matcher super String> matcher;
+
+ private WithName(Matcher super String> matcher) {
+ this.matcher = matcher;
+ }
+
+ @Override
+ protected boolean matchesSafely(MetricID item) {
+ return matcher.matches(item.getName());
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("metric ID name");
+ description.appendDescriptionOf(matcher);
+ }
+ }
+}
diff --git a/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestClient.java b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestClient.java
new file mode 100644
index 00000000000..84a932c60d7
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestClient.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HEAD;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+
+public interface TestClient {
+
+ @GET
+ @Path("/get")
+ String get();
+
+ @PUT
+ @Path("put")
+ String put(String message);
+
+ @HEAD
+ @Path("/unannotatedRestMethod")
+ void unmeasuredRestMethod();
+}
diff --git a/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestScanning.java b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestScanning.java
new file mode 100644
index 00000000000..3869c4885a7
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestScanning.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import io.helidon.microprofile.testing.junit5.AddBean;
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HEAD;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+import org.eclipse.microprofile.metrics.Counter;
+import org.eclipse.microprofile.metrics.MetricID;
+import org.eclipse.microprofile.metrics.MetricRegistry;
+import org.eclipse.microprofile.metrics.Timer;
+import org.eclipse.microprofile.metrics.annotation.Counted;
+import org.eclipse.microprofile.metrics.annotation.Timed;
+import org.eclipse.microprofile.rest.client.RestClientBuilder;
+import org.junit.jupiter.api.BeforeEach;
+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.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+@HelidonTest
+@AddBean(TestScanning.ServiceClient.class)
+@AddBean(TestScanning.ServiceClientParent.class)
+@AddBean(TestService.class)
+class TestScanning {
+
+ @Inject
+ WebTarget webTarget;
+
+ private ServiceClient serviceClient;
+
+ @BeforeEach
+ void init() {
+ serviceClient = RestClientBuilder.newBuilder()
+ .baseUri(webTarget.getUri())
+ .build(ServiceClient.class);
+ }
+
+ @Test
+ void annotationsOnMethods() throws NoSuchMethodException {
+ RestClientMetricsCdiExtension extension = CDI.current().getBeanManager()
+ .getExtension(RestClientMetricsCdiExtension.class);
+ Map> filterWorkByMethod =
+ extension.metricsUpdateWorkByMethod();
+ assertThat("Check for expected filter work", filterWorkByMethod.keySet(),
+ allOf(hasItems(equalTo(ServiceClient.class.getMethod("get")),
+ equalTo(ServiceClient.class.getMethod("put"))),
+ not(hasItems(equalTo(ServiceClient.class.getMethod("timedNonRestMethod")),
+ equalTo(ServiceClient.class.getMethod("countedNonRestMethod")),
+ equalTo(ServiceClient.class.getMethod("unannotatedRestMethod"))))));
+ }
+
+ @Test
+ void checkMetricsRegistrationsFromMethods() {
+ MetricRegistry metricRegistry = CDI.current().select(MetricRegistry.class).get();
+
+ // get method
+
+ // relative, automatic name = declaring-class.element-name
+ Timer getTimer = metricRegistry.getTimer(new MetricID(ServiceClient.class.getCanonicalName() + ".get"));
+ assertThat("Relative automatically-named timer for get", getTimer, notNullValue());
+
+ // absolute, explicit name = specified-name
+ Counter getCounter = metricRegistry.getCounter(new MetricID("getAbs"));
+ assertThat("Absolute explicitly-named counter for get", getCounter, notNullValue());
+
+ // get2 method
+
+ // absolute, automatic name = element-name
+ Timer get2Timer = metricRegistry.getTimer(new MetricID("get2"));
+ assertThat("Absolute automatically-named timer for get2", get2Timer, notNullValue());
+
+ // relative, explicit name = declaring-class.specified-name
+ Counter get2Counter = metricRegistry.getCounter(new MetricID(ServiceClient.class.getCanonicalName() + ".relget2"));
+ assertThat("Relative explicitly-named counter for get2", get2Counter, notNullValue());
+
+ // timedNonRestMethod
+ Timer timedNonRestMethodTimer = metricRegistry.getTimer(new MetricID(ServiceClient.class.getCanonicalName() +
+ ".timedNonRestMethod"));
+ assertThat("Relative automatically-named timer for non-REST method", timedNonRestMethodTimer, nullValue());
+
+ // put method
+
+ // relative, automatic name = declaring-class.element-name
+ Counter putCounter = metricRegistry.getCounter(new MetricID(ServiceClient.class.getCanonicalName() + ".put"));
+ assertThat("Relative automatically-named counter for put", putCounter, notNullValue());
+
+ // absolute, explicit name = specified-name
+ Timer putTimer = metricRegistry.getTimer(new MetricID("putAbs"));
+ assertThat("Absolute explicitly-named timer for put", putTimer, notNullValue());
+
+ // countedNonRestMethod
+ Counter nonRestMethodCounter = metricRegistry.getCounter(new MetricID("shouldNotAppear"));
+ assertThat("Counter for non-REST method", nonRestMethodCounter, nullValue());
+
+ // non-REST and unmeasured
+ var metrics = metricRegistry.getMetrics();
+ assertThat("Metrics that should not appear",
+ metrics.keySet(),
+ allOf(not(contains("shouldNotAppear")),
+ not(contains("countedNonRestMethod"))));
+ }
+
+ @Test
+ void checkMetricsRegistrationsFromType() {
+ MetricRegistry metricRegistry = CDI.current().select(MetricRegistry.class).get();
+
+ // get method
+
+ // type-level absolute with explicit name = specified-name.method-name
+ Timer getTimerFromType = metricRegistry.getTimer(new MetricID("typeLevelAbs.get"));
+ assertThat("Type-level timer with absolute explicit name", getTimerFromType, notNullValue());
+
+ // type-level relative with explicit name = package.specified-name.method-name
+ Counter getCounterFromType = metricRegistry.getCounter(new MetricID(ServiceClient.class.getPackageName()
+ + ".typeLevelRel.get"));
+ assertThat("Type-level counter with relative explicit name", getCounterFromType, notNullValue());
+
+ Counter unmeasuredGetCounterFromType = metricRegistry.getCounter(new MetricID(ServiceClient.class.getPackageName()
+ + ".typeLevelRel"
+ + ".unannotatedRestMethod"));
+ assertThat("unmeasuredRest counter from type-level annotation", unmeasuredGetCounterFromType, notNullValue());
+ }
+
+ @Test
+ void checkMetricsUpdates() {
+
+ MetricRegistry metricRegistry = CDI.current().select(MetricRegistry.class).get();
+
+ List timers = new ArrayList<>();
+
+ // All on the get method.
+
+ // relative automatically-named timer on the get method in the subinterface = subtype.method-name
+ TimerInfo getTimerInfo = TimerInfo.create(metricRegistry, ServiceClient.class.getCanonicalName() + ".get");
+ assertThat("Relative automatically-named subinterface method-level timer for get method", getTimerInfo, notNullValue());
+ Duration elapsedTimeBefore = getTimerInfo.timer.getElapsedTime();
+
+ timers.add(getTimerInfo);
+
+ // absolute explicitly-named timer on the subinterface = specified-value.method-name
+
+ TimerInfo getTimerFromTypeInfo = TimerInfo.create(metricRegistry, "typeLevelAbs.get");
+
+ assertThat("Absolute explicitly-named timer from the subinterface", getTimerFromTypeInfo.timer, notNullValue());
+
+ timers.add(getTimerFromTypeInfo);
+
+ // relative automatically-named timer on parentGet method on superinterface = subtype.method-name
+ TimerInfo getTimerFromSuperTypeInfo = TimerInfo.create(metricRegistry, ServiceClient.class.getCanonicalName()
+ + ".get");
+ assertThat("Relative automatically-named timer on method in superinterface", getTimerFromSuperTypeInfo.timer,
+ notNullValue());
+
+ timers.add(getTimerFromSuperTypeInfo);
+
+ // relatively explicitly-named timer on superinterface = subtype-package.specified-name.method-name
+ TimerInfo inheritedGetMethodTimerInfo = TimerInfo.create(metricRegistry, ServiceClient.class.getPackageName()
+ + ".parentLevelRel.get");
+ assertThat("Inherited relative auto-named type-level timer for get method",
+ inheritedGetMethodTimerInfo.timer,
+ notNullValue());
+
+ timers.add(inheritedGetMethodTimerInfo);
+
+ String timedGetResult = serviceClient.get();
+
+ assertThat("Timed get result", timedGetResult, equalTo("get"));
+ Duration elapsedTimeAfter = getTimerInfo.timer.getElapsedTime();
+ assertThat("Timer delta", elapsedTimeAfter.compareTo(elapsedTimeBefore), greaterThan(0));
+
+ String parentGetResult = serviceClient.parentGet();
+ assertThat("Parent get result", parentGetResult, equalTo("parent get"));
+
+ for (TimerInfo timerInfo : timers) {
+ assertThat("Timer for timer info " + timerInfo.metricId, timerInfo.timer, notNullValue());
+ assertThat("Counter update for " + timerInfo.metricId,
+ timerInfo.timer.getCount(),
+ greaterThan(timerInfo.beforeCount));
+ }
+ }
+
+ @Test
+ void checkInheritance() {
+ MetricRegistry metricRegistry = CDI.current().select(MetricRegistry.class).get();
+
+ // parentGet method
+
+ // method-level relative automatic name = declaring-class.method-name (declaring class is the superinterface)
+ Timer parentGetMethodTimer = metricRegistry.getTimer(new MetricID(ServiceClientParent.class.getCanonicalName()
+ + ".parentGet"));
+ assertThat("Relative automatically-named timer for inherited parentGet method", parentGetMethodTimer, notNullValue());
+
+ // get method
+
+ // method-level absolute explicit name = specified-name
+ Counter getMethodCounterFromParent = metricRegistry.getCounter(new MetricID("parentGetAbs"));
+ assertThat("Absolute explicitly-named counter for inherited get method", getMethodCounterFromParent, notNullValue());
+
+ // type-level relative explicit name = package-of-declaring-class.specified-name.method-name
+ Timer superTypeLevelTimerForGet = metricRegistry.getTimer(new MetricID(ServiceClientParent.class.getPackageName() +
+ ".parentLevelRel.get"));
+ assertThat("Type-level relative explicitly-named counter for inherited get method",
+ superTypeLevelTimerForGet,
+ notNullValue());
+
+ // put method
+
+ // type-level absolute explicit name = specified-name.method-name
+ Counter inheritedPutMethodCounter = metricRegistry.getCounter(new MetricID("parentLevelAbs.put"));
+ assertThat("Type-level absolute explicitly-named counter inherited for put method",
+ inheritedPutMethodCounter,
+ notNullValue());
+ }
+
+ @Timed(name = "parentLevelRel")
+ @Counted(name = "parentLevelAbs", absolute = true)
+ interface ServiceClientParent {
+
+ @Timed
+ @GET
+ @Path("/parentGet")
+ String parentGet();
+
+ @Counted(name = "parentGetAbs", absolute = true)
+ @GET
+ @Path("/get")
+ String get();
+
+ }
+
+ @Path(TestService.RESOURCE_PATH)
+ @Counted(name = "typeLevelRel")
+ @Timed(name = "typeLevelAbs", absolute = true)
+ interface ServiceClient extends ServiceClientParent {
+
+ @Timed
+ @Counted(absolute = true, name = "getAbs")
+ @GET
+ @Path("/get")
+ String get();
+
+ @Timed(absolute = true)
+ @Counted(name = "relget2")
+ @GET
+ @Path("/get2")
+ String get2();
+
+ @Timed
+ void timedNonRestMethod();
+
+ @Counted
+ @Timed(absolute = true, name = "putAbs")
+ @PUT
+ @Path("/put")
+ void put();
+
+ @Counted(name = "shouldNotAppear", absolute = true)
+ void countedNonRestMethod();
+
+ @HEAD
+ @Path("/unannotatedRestMethod")
+ void unannotatedRestMethod();
+ }
+
+ private record TimerInfo(Timer timer, MetricID metricId, long beforeCount) {
+
+ static TimerInfo create(MetricRegistry metricRegistry, String metricName) {
+ MetricID metricID = new MetricID(metricName);
+ Timer timer = metricRegistry.getTimer(metricID);
+ return new TimerInfo(timer, metricID, timer != null ? timer.getCount() : 0L);
+ }
+ }
+
+}
diff --git a/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestService.java b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestService.java
new file mode 100644
index 00000000000..2ed8ad4b690
--- /dev/null
+++ b/microprofile/rest-client-metrics/src/test/java/io/helidon/microprofile/restclientmetrics/TestService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 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.
+ * 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.microprofile.restclientmetrics;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.HEAD;
+import jakarta.ws.rs.PUT;
+import jakarta.ws.rs.Path;
+
+@Path(TestService.RESOURCE_PATH)
+public class TestService {
+
+ static final String RESOURCE_PATH = "/restClientMetricsTest";
+
+ @GET
+ @Path("/get")
+ public String get() {
+ return "get";
+ }
+
+ @PUT
+ @Path("/put")
+ public String put(String message) {
+ return "I got " + message;
+ }
+
+ @HEAD
+ @Path("/unannotatedRestMethod")
+ public void unmeasuredRestMethod() {
+
+ }
+
+ @GET
+ @Path("/parentGet")
+ public String parentGet() {
+ return "parent get";
+ }
+
+}