You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by re...@apache.org on 2020/11/13 00:30:58 UTC

[cxf] branch 3.4.x-fixes updated (19b6ca0 -> d381ecd)

This is an automated email from the ASF dual-hosted git repository.

reta pushed a change to branch 3.4.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git.


    from 19b6ca0  Recording .gitmergeinfo Changes
     new c1b52e9  CXF-8362: Add Micrometer metric support for JAX-RS / JAX-WS (Client) (#722)
     new d381ecd  Update to Spring Boot 2.3.6.RELEASE

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../spring/boot/autoconfigure/CxfProperties.java   |  34 +++
 .../MicrometerMetricsAutoConfiguration.java        |  16 +-
 .../SpringBasedTimedAnnotationProvider.java        |  12 +-
 .../SpringBasedTimedAnnotationProviderTest.java    |  16 +-
 parent/pom.xml                                     |   4 +-
 .../micrometer/MicrometerClientMetricsContext.java |  58 +++++
 .../micrometer/MicrometerMetricsContext.java       |  53 ++--
 .../micrometer/MicrometerMetricsProperties.java    |  13 +
 .../micrometer/MicrometerMetricsProvider.java      |  24 +-
 .../micrometer/MicrometerServerMetricsContext.java |  58 +++++
 .../provider/DefaultExceptionClassProvider.java    |  27 +-
 .../provider/DefaultTimedAnnotationProvider.java   |   8 +-
 .../provider/ExceptionClassProvider.java           |   2 +-
 .../metrics/micrometer/provider/StandardTags.java  |  29 ++-
 .../micrometer/provider/StandardTagsProvider.java  |  24 +-
 .../micrometer/provider/TagsCustomizer.java        |   2 +-
 .../metrics/micrometer/provider/TagsProvider.java  |   2 +-
 .../provider/TimedAnnotationProvider.java          |   2 +-
 .../jaxrs/JaxrsOperationTagsCustomizer.java        |  12 +-
 .../provider/jaxws/JaxwsFaultCodeProvider.java     |  23 +-
 .../jaxws/JaxwsFaultCodeTagsCustomizer.java        |   4 +-
 .../jaxws/JaxwsOperationTagsCustomizer.java        |  12 +-
 ...ava => MicrometerClientMetricsContextTest.java} |  24 +-
 .../micrometer/MicrometerMetricsProviderTest.java  |  30 ++-
 ...ava => MicrometerServerMetricsContextTest.java} |  18 +-
 .../DefaultExceptionClassProviderTest.java         |  15 +-
 .../DefaultTimedAnnotationProviderTest.java        |  12 +-
 .../provider/StandardTagsProviderTest.java         |   8 +-
 .../jaxrs/JaxrsOperationTagsCustomizerTest.java    |   4 +-
 .../provider/jaxws/JaxwsFaultCodeProviderTest.java |  16 +-
 .../jaxws/JaxwsFaultCodeTagsCustomizerTest.java    |   8 +-
 .../jaxws/JaxwsOperationTagsCustomizerTest.java    |   8 +-
 .../micrometer/provider/jaxws/JaxwsTagsTest.java   |   4 +-
 .../cxf/systest/jaxrs/resources/Library.java       |  22 +-
 .../cxf/systest/jaxrs/resources/LibraryApi.java    |  23 +-
 .../systest/jaxrs/spring/boot/SpringJaxrsTest.java | 288 ++++++++++++++++++---
 .../systest/jaxws/spring/boot/SpringJaxwsTest.java | 204 +++++++++++++--
 37 files changed, 884 insertions(+), 235 deletions(-)
 create mode 100644 rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContext.java
 create mode 100644 rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContext.java
 copy rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/{MicrometerMetricsContextTest.java => MicrometerClientMetricsContextTest.java} (92%)
 rename rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/{MicrometerMetricsContextTest.java => MicrometerServerMetricsContextTest.java} (95%)
 copy rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/BaseApi.java => systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/LibraryApi.java (71%)


[cxf] 02/02: Update to Spring Boot 2.3.6.RELEASE

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.4.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit d381ecd46b08e3f6fde15960e5198a3fce49f271
Author: reta <dr...@gmail.com>
AuthorDate: Thu Nov 12 19:25:24 2020 -0500

    Update to Spring Boot 2.3.6.RELEASE
    
    (cherry picked from commit dde79e4471e12ab53dd87a0d2adee105c57b0a71)
---
 parent/pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/parent/pom.xml b/parent/pom.xml
index c78bf93..749e288 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -194,7 +194,7 @@
         <cxf.slf4j.version>1.7.30</cxf.slf4j.version>
         <cxf.snakeyaml.version>1.26</cxf.snakeyaml.version>
         <cxf.specs.jaxws.api.version>2.3_2</cxf.specs.jaxws.api.version>
-        <cxf.spring.boot.version>2.3.5.RELEASE</cxf.spring.boot.version>
+        <cxf.spring.boot.version>2.3.6.RELEASE</cxf.spring.boot.version>
         <cxf.spring.ldap.version>2.3.3.RELEASE</cxf.spring.ldap.version>
         <cxf.spring.mock>spring-test</cxf.spring.mock>
         <cxf.spring.osgi.version>1.2.1</cxf.spring.osgi.version>


[cxf] 01/02: CXF-8362: Add Micrometer metric support for JAX-RS / JAX-WS (Client) (#722)

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.4.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit c1b52e9ed11541a74d1c7d48e880052c1d38049f
Author: Andriy Redko <dr...@gmail.com>
AuthorDate: Thu Nov 12 18:50:43 2020 -0500

    CXF-8362: Add Micrometer metric support for JAX-RS / JAX-WS (Client) (#722)
    
    (cherry picked from commit 3c855f79bfe7114e496d2d4c67a1815b22eebe6f)
---
 .../spring/boot/autoconfigure/CxfProperties.java   |  34 +++
 .../MicrometerMetricsAutoConfiguration.java        |  16 +-
 .../SpringBasedTimedAnnotationProvider.java        |  12 +-
 .../SpringBasedTimedAnnotationProviderTest.java    |  16 +-
 parent/pom.xml                                     |   2 +-
 .../micrometer/MicrometerClientMetricsContext.java |  58 +++++
 .../micrometer/MicrometerMetricsContext.java       |  53 ++--
 .../micrometer/MicrometerMetricsProperties.java    |  13 +
 .../micrometer/MicrometerMetricsProvider.java      |  24 +-
 .../micrometer/MicrometerServerMetricsContext.java |  58 +++++
 .../provider/DefaultExceptionClassProvider.java    |  27 +-
 .../provider/DefaultTimedAnnotationProvider.java   |   8 +-
 .../provider/ExceptionClassProvider.java           |   2 +-
 .../metrics/micrometer/provider/StandardTags.java  |  29 ++-
 .../micrometer/provider/StandardTagsProvider.java  |  24 +-
 .../micrometer/provider/TagsCustomizer.java        |   2 +-
 .../metrics/micrometer/provider/TagsProvider.java  |   2 +-
 .../provider/TimedAnnotationProvider.java          |   2 +-
 .../jaxrs/JaxrsOperationTagsCustomizer.java        |  12 +-
 .../provider/jaxws/JaxwsFaultCodeProvider.java     |  23 +-
 .../jaxws/JaxwsFaultCodeTagsCustomizer.java        |   4 +-
 .../jaxws/JaxwsOperationTagsCustomizer.java        |  12 +-
 ...ava => MicrometerClientMetricsContextTest.java} |  24 +-
 .../micrometer/MicrometerMetricsProviderTest.java  |  30 ++-
 ...ava => MicrometerServerMetricsContextTest.java} |  18 +-
 .../DefaultExceptionClassProviderTest.java         |  15 +-
 .../DefaultTimedAnnotationProviderTest.java        |  12 +-
 .../provider/StandardTagsProviderTest.java         |   8 +-
 .../jaxrs/JaxrsOperationTagsCustomizerTest.java    |   4 +-
 .../provider/jaxws/JaxwsFaultCodeProviderTest.java |  16 +-
 .../jaxws/JaxwsFaultCodeTagsCustomizerTest.java    |   8 +-
 .../jaxws/JaxwsOperationTagsCustomizerTest.java    |   8 +-
 .../micrometer/provider/jaxws/JaxwsTagsTest.java   |   4 +-
 .../cxf/systest/jaxrs/resources/Library.java       |  22 +-
 .../cxf/systest/jaxrs/resources/LibraryApi.java    |  38 +--
 .../systest/jaxrs/spring/boot/SpringJaxrsTest.java | 288 ++++++++++++++++++---
 .../systest/jaxws/spring/boot/SpringJaxwsTest.java | 204 +++++++++++++--
 37 files changed, 890 insertions(+), 242 deletions(-)

diff --git a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/CxfProperties.java b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/CxfProperties.java
index 016851d..7f18726 100644
--- a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/CxfProperties.java
+++ b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/CxfProperties.java
@@ -95,6 +95,7 @@ public class CxfProperties {
 
     public static class Metrics {
         private final Server server = new Server();
+        private final Client client = new Client();
         
         /**
          * Enables or disables metrics instrumentation
@@ -104,6 +105,10 @@ public class CxfProperties {
         public Server getServer() {
             return this.server;
         }
+        
+        public Client getClient() {
+            return this.client;
+        }
 
         public static class Server {
 
@@ -150,6 +155,35 @@ public class CxfProperties {
             }
         }
         
+        public static class Client {
+            /**
+             * Name of the metric for sent requests.
+             */
+            private String requestsMetricName = "cxf.client.requests";
+            
+            /**
+             * Maximum number of unique URI tag values allowed. After the max number of tag values is
+             * reached, metrics with additional tag values are denied by filter.
+             */
+            private int maxUriTags = 100;
+
+            public String getRequestsMetricName() {
+                return this.requestsMetricName;
+            }
+
+            public void setRequestsMetricName(String requestsMetricName) {
+                this.requestsMetricName = requestsMetricName;
+            }
+            
+            public int getMaxUriTags() {
+                return this.maxUriTags;
+            }
+
+            public void setMaxUriTags(int maxUriTags) {
+                this.maxUriTags = maxUriTags;
+            }
+        }
+        
         public boolean isEnabled() {
             return enabled;
         }
diff --git a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/MicrometerMetricsAutoConfiguration.java b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/MicrometerMetricsAutoConfiguration.java
index 89aee36..2a19231 100644
--- a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/MicrometerMetricsAutoConfiguration.java
+++ b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/MicrometerMetricsAutoConfiguration.java
@@ -40,6 +40,7 @@ import org.apache.cxf.metrics.micrometer.provider.jaxws.JaxwsFaultCodeTagsCustom
 import org.apache.cxf.metrics.micrometer.provider.jaxws.JaxwsOperationTagsCustomizer;
 import org.apache.cxf.metrics.micrometer.provider.jaxws.JaxwsTags;
 import org.apache.cxf.spring.boot.autoconfigure.CxfProperties;
+import org.apache.cxf.spring.boot.autoconfigure.CxfProperties.Metrics.Client;
 import org.apache.cxf.spring.boot.autoconfigure.CxfProperties.Metrics.Server;
 import org.apache.cxf.spring.boot.autoconfigure.micrometer.provider.SpringBasedTimedAnnotationProvider;
 import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
@@ -102,9 +103,12 @@ public class MicrometerMetricsAutoConfiguration {
                                            MeterRegistry registry) {
         MicrometerMetricsProperties micrometerMetricsProperties = new MicrometerMetricsProperties();
 
-        Server server = this.properties.getMetrics().getServer();
+        final Server server = this.properties.getMetrics().getServer();
         micrometerMetricsProperties.setAutoTimeRequests(server.isAutoTimeRequests());
         micrometerMetricsProperties.setServerRequestsMetricName(server.getRequestsMetricName());
+        
+        final Client client = this.properties.getMetrics().getClient();
+        micrometerMetricsProperties.setClientRequestsMetricName(client.getRequestsMetricName());
 
         return new MicrometerMetricsProvider(registry, tagsProvider, tagsCustomizers, timedAnnotationProvider,
                 micrometerMetricsProperties);
@@ -120,6 +124,16 @@ public class MicrometerMetricsAutoConfiguration {
                 metricName, "uri", this.properties.getMetrics().getServer().getMaxUriTags(), filter);
     }
     
+    @Bean
+    @Order(0)
+    public MeterFilter cxfMetricsMaxAllowedClientUriTagsFilter() {
+        String metricName = this.properties.getMetrics().getClient().getRequestsMetricName();
+        MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
+        () -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
+        return MeterFilter.maximumAllowableTags(
+                metricName, "uri", this.properties.getMetrics().getClient().getMaxUriTags(), filter);
+    }
+    
     @Configuration
     @ConditionalOnClass(JaxWsServerFactoryBean.class)
     @ConditionalOnProperty(name = "cxf.metrics.jaxws.enabled", matchIfMissing = true)
diff --git a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProvider.java b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProvider.java
index c8ee5a2..3533e23 100644
--- a/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProvider.java
+++ b/integration/spring-boot/autoconfigure/src/main/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProvider.java
@@ -43,8 +43,8 @@ public class SpringBasedTimedAnnotationProvider implements TimedAnnotationProvid
     private final ConcurrentHashMap<HandlerMethod, Set<Timed>> timedAnnotationCache = new ConcurrentHashMap<>();
 
     @Override
-    public Set<Timed> getTimedAnnotations(Exchange ex) {
-        HandlerMethod handlerMethod = HandlerMethod.create(ex);
+    public Set<Timed> getTimedAnnotations(Exchange ex, boolean client) {
+        HandlerMethod handlerMethod = HandlerMethod.create(ex, client);
         if (handlerMethod == null) {
             return emptySet();
         }
@@ -80,14 +80,16 @@ public class SpringBasedTimedAnnotationProvider implements TimedAnnotationProvid
             this.method = method;
         }
 
-        private static HandlerMethod create(Exchange exchange) {
+        private static HandlerMethod create(Exchange exchange, boolean client) {
             final Service service = exchange.getService();
             if (service != null) {
                 final BindingOperationInfo bop = exchange.getBindingOperationInfo();
                 if (bop != null) { /* JAX-WS call */
                     final MethodDispatcher md = (MethodDispatcher) service.get(MethodDispatcher.class.getName());
-                    final Method method = md.getMethod(bop);
-                    return new HandlerMethod(method.getDeclaringClass(), method);
+                    if (md != null) { /* may be 'null' on client side */
+                        final Method method = md.getMethod(bop);
+                        return new HandlerMethod(method.getDeclaringClass(), method);
+                    }
                 } else { /* JAX-RS call */
                     final OperationResourceInfo ori = exchange.get(OperationResourceInfo.class);
                     if (ori != null) {
diff --git a/integration/spring-boot/autoconfigure/src/test/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProviderTest.java b/integration/spring-boot/autoconfigure/src/test/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProviderTest.java
index ee0c4e0..93d4f0f 100644
--- a/integration/spring-boot/autoconfigure/src/test/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProviderTest.java
+++ b/integration/spring-boot/autoconfigure/src/test/java/org/apache/cxf/spring/boot/autoconfigure/micrometer/provider/SpringBasedTimedAnnotationProviderTest.java
@@ -39,7 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.empty;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 @SuppressWarnings({"unused"})
 public class SpringBasedTimedAnnotationProviderTest {
@@ -55,7 +55,7 @@ public class SpringBasedTimedAnnotationProviderTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
         underTest = new SpringBasedTimedAnnotationProvider();
 
         doReturn(service).when(exchange).getService();
@@ -76,7 +76,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -96,7 +96,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -115,7 +115,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), containsInAnyOrder("aliasTimed"));
@@ -134,7 +134,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -155,7 +155,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), containsInAnyOrder("timed2"));
@@ -176,7 +176,7 @@ public class SpringBasedTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), empty());
diff --git a/parent/pom.xml b/parent/pom.xml
index 4d23695..c78bf93 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -162,7 +162,7 @@
         <cxf.logback.classic.version>1.2.3</cxf.logback.classic.version>
         <cxf.lucene.version>8.2.0</cxf.lucene.version>
         <cxf.maven.core.version>3.6.3</cxf.maven.core.version>
-        <cxf.micrometer.version>1.5.5</cxf.micrometer.version>
+        <cxf.micrometer.version>1.5.7</cxf.micrometer.version>
         <cxf.microprofile.config.version>1.2</cxf.microprofile.config.version>
         <cxf.microprofile.rest.client.version>2.0-RC2</cxf.microprofile.rest.client.version>
         <cxf.microprofile.openapi.version>1.1.2</cxf.microprofile.openapi.version>        
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContext.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContext.java
new file mode 100644
index 0000000..323e9e1
--- /dev/null
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContext.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.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 org.apache.cxf.metrics.micrometer;
+
+import java.util.List;
+
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.metrics.micrometer.provider.TagsCustomizer;
+import org.apache.cxf.metrics.micrometer.provider.TagsProvider;
+import org.apache.cxf.metrics.micrometer.provider.TimedAnnotationProvider;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+
+public class MicrometerClientMetricsContext extends MicrometerMetricsContext {
+    public MicrometerClientMetricsContext(MeterRegistry registry, TagsProvider tagsProvider,
+                                    TimedAnnotationProvider timedAnnotationProvider,
+                                    List<TagsCustomizer> tagsCustomizers, String metricName) {
+        super(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers, metricName, true);
+    }
+
+    @Override
+    public void start(Exchange ex) {
+        super.start(ex.getOutMessage(), ex);
+    }
+
+    @Override
+    public void stop(long timeInNS, long inSize, long outSize, Exchange ex) {
+        super.stop(ex.getOutMessage(), timeInNS, inSize, outSize, ex);
+    }
+    
+    @Override
+    protected Iterable<Tag> getAllTags(Exchange ex) {
+        return getAllTags(ex, true);
+    }
+    
+    @Override
+    protected void record(TimingContext timingContext, Exchange ex) {
+        super.record(timingContext, ex, true);
+    }
+}
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContext.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContext.java
index f15e8e2..955b016 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContext.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContext.java
@@ -42,10 +42,8 @@ import io.micrometer.core.instrument.Timer;
 import static java.util.stream.Stream.concat;
 import static java.util.stream.StreamSupport.stream;
 
-public class MicrometerMetricsContext implements MetricsContext {
-
-    private static final Logger LOG =
-            LogUtils.getL7dLogger(MicrometerMetricsContext.class);
+abstract class MicrometerMetricsContext implements MetricsContext {
+    private static final Logger LOG = LogUtils.getL7dLogger(MicrometerMetricsContext.class);
 
     private final MeterRegistry registry;
     private final TagsProvider tagsProvider;
@@ -55,9 +53,9 @@ public class MicrometerMetricsContext implements MetricsContext {
     private final String metricName;
     private final boolean autoTimeRequests;
 
-    public MicrometerMetricsContext(MeterRegistry registry, TagsProvider tagsProvider,
-                                    TimedAnnotationProvider timedAnnotationProvider,
-                                    List<TagsCustomizer> tagsCustomizers, String metricName, boolean autoTimeRequests) {
+    MicrometerMetricsContext(MeterRegistry registry, TagsProvider tagsProvider,
+                             TimedAnnotationProvider timedAnnotationProvider,
+                             List<TagsCustomizer> tagsCustomizers, String metricName, boolean autoTimeRequests) {
         this.registry = registry;
         this.tagsProvider = tagsProvider;
         this.timedAnnotationProvider = timedAnnotationProvider;
@@ -66,18 +64,14 @@ public class MicrometerMetricsContext implements MetricsContext {
         this.autoTimeRequests = autoTimeRequests;
     }
 
-    @Override
-    public void start(Exchange ex) {
-        Message request = ex.getInMessage();
+    protected void start(Message request, Exchange ex) {
         TimingContext timingContext = TimingContext.get(request);
         if (timingContext == null) {
             startAndAttachTimingContext(request);
         }
     }
 
-    @Override
-    public void stop(long timeInNS, long inSize, long outSize, Exchange ex) {
-        Message request = ex.getInMessage();
+    protected void stop(Message request, long timeInNS, long inSize, long outSize, Exchange ex) {
         TimingContext timingContext = TimingContext.get(request);
         if (timingContext == null) {
             LOG.warning("Unable for record metric for exchange: " + ex);
@@ -85,15 +79,23 @@ public class MicrometerMetricsContext implements MetricsContext {
             record(timingContext, ex);
         }
     }
+    
+    protected abstract Iterable<Tag> getAllTags(Exchange ex);
+    protected abstract void record(TimingContext timingContext, Exchange ex);
+    
+    protected Iterable<Tag> getAllTags(Exchange ex, boolean client) {
+        Stream<Tag> defaultTags = getStreamFrom(this.tagsProvider.getTags(ex, client));
+        Stream<Tag> additionalTags =
+                tagsCustomizers.stream()
+                        .map(tagsCustomizer -> tagsCustomizer.getAdditionalTags(ex, client))
+                        .flatMap(this::getStreamFrom);
 
-    private void startAndAttachTimingContext(Message request) {
-        Timer.Sample timerSample = Timer.start(this.registry);
-        TimingContext timingContext = new TimingContext(timerSample);
-        timingContext.attachTo(request);
+        return concat(defaultTags, additionalTags)
+                .collect(Collectors.toList());
     }
 
-    private void record(TimingContext timingContext, Exchange ex) {
-        Set<Timed> annotations = timedAnnotationProvider.getTimedAnnotations(ex);
+    protected void record(TimingContext timingContext, Exchange ex, boolean client) {
+        Set<Timed> annotations = timedAnnotationProvider.getTimedAnnotations(ex, client);
         Timer.Sample timerSample = timingContext.getTimerSample();
         Supplier<Iterable<Tag>> tags = () -> getAllTags(ex);
 
@@ -108,15 +110,10 @@ public class MicrometerMetricsContext implements MetricsContext {
         }
     }
 
-    private Iterable<Tag> getAllTags(Exchange ex) {
-        Stream<Tag> defaultTags = getStreamFrom(this.tagsProvider.getTags(ex));
-        Stream<Tag> additionalTags =
-                tagsCustomizers.stream()
-                        .map(tagsCustomizer -> tagsCustomizer.getAdditionalTags(ex))
-                        .flatMap(this::getStreamFrom);
-
-        return concat(defaultTags, additionalTags)
-                .collect(Collectors.toList());
+    private void startAndAttachTimingContext(Message request) {
+        Timer.Sample timerSample = Timer.start(this.registry);
+        TimingContext timingContext = new TimingContext(timerSample);
+        timingContext.attachTo(request);
     }
 
     private Stream<Tag> getStreamFrom(Iterable<Tag> tags) {
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProperties.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProperties.java
index 6573392..99b1993 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProperties.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProperties.java
@@ -32,6 +32,11 @@ public class MicrometerMetricsProperties {
      * Name of the metric for received requests.
      */
     private String serverRequestsMetricName = "cxf.server.requests";
+    
+    /**
+     * Name of the metric for sent requests.
+     */
+    private String clientRequestsMetricName = "cxf.client.requests";
 
     public boolean isAutoTimeRequests() {
         return autoTimeRequests;
@@ -48,4 +53,12 @@ public class MicrometerMetricsProperties {
     public void setServerRequestsMetricName(String requestsMetricName) {
         this.serverRequestsMetricName = requestsMetricName;
     }
+
+    public String getClientRequestsMetricName() {
+        return clientRequestsMetricName;
+    }
+
+    public void setClientRequestsMetricName(String clientRequestsMetricName) {
+        this.clientRequestsMetricName = clientRequestsMetricName;
+    }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProvider.java
index d4c0601..99f553f 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProvider.java
@@ -70,14 +70,14 @@ public class MicrometerMetricsProvider implements MetricsProvider {
     @Override
     public MetricsContext createOperationContext(Endpoint endpoint, BindingOperationInfo boi, boolean asClient,
                                                  String clientId) {
-        // Client metrics are not yet supported
         if (asClient) {
-            return null;
+            return new MicrometerClientMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
+                micrometerMetricsProperties.getClientRequestsMetricName());
+        } else {
+            return new MicrometerServerMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
+                micrometerMetricsProperties.getServerRequestsMetricName(), 
+                micrometerMetricsProperties.isAutoTimeRequests());
         }
-        
-        return new MicrometerMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
-            micrometerMetricsProperties.getServerRequestsMetricName(), 
-            micrometerMetricsProperties.isAutoTimeRequests());
     }
 
 
@@ -87,13 +87,13 @@ public class MicrometerMetricsProvider implements MetricsProvider {
     @Override
     public MetricsContext createResourceContext(Endpoint endpoint, String resourceName, boolean asClient,
                                                 String clientId) {
-        // Client metrics are not yet supported
         if (asClient) {
-            return null;
+            return new MicrometerClientMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
+                micrometerMetricsProperties.getClientRequestsMetricName());
+        } else {
+            return new MicrometerServerMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
+                micrometerMetricsProperties.getServerRequestsMetricName(), 
+                micrometerMetricsProperties.isAutoTimeRequests());
         }
-        
-        return new MicrometerMetricsContext(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers,
-            micrometerMetricsProperties.getServerRequestsMetricName(), 
-            micrometerMetricsProperties.isAutoTimeRequests());
     }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContext.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContext.java
new file mode 100644
index 0000000..859d842
--- /dev/null
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContext.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.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 org.apache.cxf.metrics.micrometer;
+
+import java.util.List;
+
+import org.apache.cxf.message.Exchange;
+import org.apache.cxf.metrics.micrometer.provider.TagsCustomizer;
+import org.apache.cxf.metrics.micrometer.provider.TagsProvider;
+import org.apache.cxf.metrics.micrometer.provider.TimedAnnotationProvider;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+
+public class MicrometerServerMetricsContext extends MicrometerMetricsContext {
+    public MicrometerServerMetricsContext(MeterRegistry registry, TagsProvider tagsProvider,
+                                    TimedAnnotationProvider timedAnnotationProvider,
+                                    List<TagsCustomizer> tagsCustomizers, String metricName, boolean autoTimeRequests) {
+        super(registry, tagsProvider, timedAnnotationProvider, tagsCustomizers, metricName, autoTimeRequests);
+    }
+
+    @Override
+    public void start(Exchange ex) {
+        super.start(ex.getInMessage(), ex);
+    }
+
+    @Override
+    public void stop(long timeInNS, long inSize, long outSize, Exchange ex) {
+        super.stop(ex.getInMessage(), timeInNS, inSize, outSize, ex);
+    }
+    
+    @Override
+    protected Iterable<Tag> getAllTags(Exchange ex) {
+        return getAllTags(ex, false);
+    }
+    
+    @Override
+    protected void record(TimingContext timingContext, Exchange ex) {
+        super.record(timingContext, ex, false);
+    }
+}
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProvider.java
index 23c6e9b..b915f68 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProvider.java
@@ -26,18 +26,29 @@ import org.apache.cxf.message.Exchange;
 public class DefaultExceptionClassProvider implements ExceptionClassProvider {
 
     @Override
-    public Class<?> getExceptionClass(Exchange ex) {
-        return getFault(ex).map(Throwable::getCause).map(Throwable::getClass).orElse(null);
+    public Class<?> getExceptionClass(Exchange ex, boolean client) {
+        return getFault(ex, client).map(Throwable::getCause).map(Throwable::getClass).orElse(null);
     }
 
-    private Optional<Throwable> getFault(Exchange ex) {
+    private Optional<Throwable> getFault(Exchange ex, boolean client) {
         Exception exception = ex.get(Exception.class);
-        if (exception == null && ex.getOutFaultMessage() != null) {
-            exception = ex.getOutFaultMessage().get(Exception.class);
-        }
-        if (exception == null && ex.getInMessage() != null) {
-            exception = ex.getInMessage().get(Exception.class);
+        
+        if (client) {
+            if (exception == null && ex.getInFaultMessage() != null) {
+                exception = ex.getInFaultMessage().get(Exception.class);
+            }
+            if (exception == null && ex.getOutMessage() != null) {
+                exception = ex.getOutMessage().get(Exception.class);
+            }
+        } else {
+            if (exception == null && ex.getOutFaultMessage() != null) {
+                exception = ex.getOutFaultMessage().get(Exception.class);
+            }
+            if (exception == null && ex.getInMessage() != null) {
+                exception = ex.getInMessage().get(Exception.class);
+            }
         }
+        
         return Optional.ofNullable(exception);
     }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProvider.java
index 5b018d0..03afd29 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProvider.java
@@ -43,8 +43,8 @@ public class DefaultTimedAnnotationProvider implements TimedAnnotationProvider {
     private final ConcurrentHashMap<HandlerMethod, Set<Timed>> timedAnnotationCache = new ConcurrentHashMap<>();
 
     @Override
-    public Set<Timed> getTimedAnnotations(Exchange ex) {
-        final HandlerMethod handlerMethod = HandlerMethod.create(ex);
+    public Set<Timed> getTimedAnnotations(Exchange ex, boolean client) {
+        final HandlerMethod handlerMethod = HandlerMethod.create(ex, client);
         if (handlerMethod == null) {
             return emptySet();
         }
@@ -103,9 +103,9 @@ public class DefaultTimedAnnotationProvider implements TimedAnnotationProvider {
             this.beanType = beanType;
         }
         
-        private static HandlerMethod create(Exchange exchange) {
+        private static HandlerMethod create(Exchange exchange, boolean client) {
             return MessageUtils
-                .getTargetMethod(exchange.getInMessage())
+                .getTargetMethod(client ? exchange.getOutMessage() : exchange.getInMessage())
                 .map(method -> new HandlerMethod(method.getDeclaringClass(), method))
                 .orElse(null);
         }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/ExceptionClassProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/ExceptionClassProvider.java
index 7e45dec..a747796 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/ExceptionClassProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/ExceptionClassProvider.java
@@ -23,5 +23,5 @@ import org.apache.cxf.message.Exchange;
 
 public interface ExceptionClassProvider {
 
-    Class<?> getExceptionClass(Exchange ex);
+    Class<?> getExceptionClass(Exchange ex, boolean client);
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTags.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTags.java
index 9b63ecf..07247a5 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTags.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTags.java
@@ -77,9 +77,9 @@ public class StandardTags {
         return ofNullable(request)
                 .map(e -> e.get(Message.REQUEST_URI))
                 .filter(e -> e instanceof String)
-                .map(e -> (String) e)
+                .map(e -> stripQueryString((String) e))
                 .map(e -> Tag.of("uri", e))
-                .orElse(URI_UNKNOWN);
+                .orElse(endpoint(request));
     }
 
     public Tag exception(Class<?> exceptionClass) {
@@ -113,4 +113,29 @@ public class StandardTags {
         }
         return OUTCOME_SERVER_ERROR;
     }
+    
+    /**
+     * The Request URI endpoint fallback in case of JAX-WS client invocations
+     */
+    private Tag endpoint(Message request) {
+        return ofNullable(request)
+                .map(e -> e.get(Message.ENDPOINT_ADDRESS))
+                .filter(e -> e instanceof String)
+                .map(e -> stripQueryString((String) e))
+                .map(e -> Tag.of("uri", e))
+                .orElse(URI_UNKNOWN);
+    }
+    
+    /**
+     * Strips the query string from the request URI
+     */
+    private String stripQueryString(String uri) {
+        final int index = uri.indexOf('?');
+        
+        if (index != -1) {
+            return uri.substring(0, index);
+        }
+        
+        return uri;
+    }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProvider.java
index 28a8fc9..ea78422 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProvider.java
@@ -38,11 +38,11 @@ public class StandardTagsProvider implements TagsProvider {
     }
 
     @Override
-    public Iterable<Tag> getTags(Exchange ex) {
-        Message request = ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
-        Message response = ofNullable(ex.getOutMessage()).orElseGet(ex::getOutFaultMessage);
+    public Iterable<Tag> getTags(Exchange ex, boolean client) {
+        final Message request = getRequest(ex, client);
+        final Message response = getResponse(ex, client);
 
-        Class<?> exception = exceptionClassProvider.getExceptionClass(ex);
+        Class<?> exception = exceptionClassProvider.getExceptionClass(ex, client);
 
         return Tags.of(
                 standardTags.method(request),
@@ -51,4 +51,20 @@ public class StandardTagsProvider implements TagsProvider {
                 standardTags.status(response),
                 standardTags.outcome(response));
     }
+
+    private Message getResponse(Exchange ex, boolean client) {
+        if (client) {
+            return ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+        } else {
+            return ofNullable(ex.getOutMessage()).orElseGet(ex::getOutFaultMessage);
+        }
+    }
+
+    private Message getRequest(Exchange ex, boolean client) {
+        if (client) {
+            return ofNullable(ex.getOutMessage()).orElseGet(ex::getOutFaultMessage);
+        } else {
+            return ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+        }
+    }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsCustomizer.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsCustomizer.java
index 607e81a..eb3362a 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsCustomizer.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsCustomizer.java
@@ -24,5 +24,5 @@ import org.apache.cxf.message.Exchange;
 import io.micrometer.core.instrument.Tag;
 
 public interface TagsCustomizer {
-    Iterable<Tag> getAdditionalTags(Exchange ex);
+    Iterable<Tag> getAdditionalTags(Exchange ex, boolean client);
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsProvider.java
index f2be18c..b556bf3 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TagsProvider.java
@@ -25,5 +25,5 @@ import io.micrometer.core.instrument.Tag;
 
 public interface TagsProvider {
 
-    Iterable<Tag> getTags(Exchange ex);
+    Iterable<Tag> getTags(Exchange ex, boolean client);
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TimedAnnotationProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TimedAnnotationProvider.java
index 384c2fe..d8d0fe2 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TimedAnnotationProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/TimedAnnotationProvider.java
@@ -27,5 +27,5 @@ import io.micrometer.core.annotation.Timed;
 
 public interface TimedAnnotationProvider {
 
-    Set<Timed> getTimedAnnotations(Exchange ex);
+    Set<Timed> getTimedAnnotations(Exchange ex, boolean client);
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizer.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizer.java
index 0a90b8a..c7e591a 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizer.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizer.java
@@ -36,8 +36,16 @@ public class JaxrsOperationTagsCustomizer implements TagsCustomizer {
     }
 
     @Override
-    public Iterable<Tag> getAdditionalTags(Exchange ex) {
-        Message request = ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+    public Iterable<Tag> getAdditionalTags(Exchange ex, boolean client) {
+        Message request = getRequest(ex, client);
         return Tags.of(jaxrsTags.operation(request));
     }
+
+    private Message getRequest(Exchange ex, boolean client) {
+        if (client) {
+            return ofNullable(ex.getOutMessage()).orElseGet(ex::getOutFaultMessage);
+        } else {
+            return ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+        }
+    }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java
index 7128abd..7b9f7f0 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java
@@ -24,17 +24,28 @@ import org.apache.cxf.message.FaultMode;
 
 public class JaxwsFaultCodeProvider {
     
-    public String getFaultCode(Exchange ex) {
+    public String getFaultCode(Exchange ex, boolean client) {
         FaultMode fm = ex.get(FaultMode.class);
-        if (fm == null && ex.getOutFaultMessage() != null) {
-            fm = ex.getOutFaultMessage().get(FaultMode.class);
-        }
-        if (fm == null && ex.getInMessage() != null) {
-            fm = ex.getInMessage().get(FaultMode.class);
+        if (client) {
+            if (fm == null && ex.getInFaultMessage() != null) {
+                fm = ex.getInFaultMessage().get(FaultMode.class);
+            }
+            if (fm == null && ex.getOutMessage() != null) {
+                fm = ex.getOutMessage().get(FaultMode.class);
+            }
+        } else {
+            if (fm == null && ex.getOutFaultMessage() != null) {
+                fm = ex.getOutFaultMessage().get(FaultMode.class);
+            }
+            if (fm == null && ex.getInMessage() != null) {
+                fm = ex.getInMessage().get(FaultMode.class);
+            }
         }
+        
         if (fm == null) {
             return null;
         }
+        
         return fm.name();
     }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizer.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizer.java
index 61dcf5c..2798322 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizer.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizer.java
@@ -36,8 +36,8 @@ public class JaxwsFaultCodeTagsCustomizer implements TagsCustomizer {
     }
 
     @Override
-    public Iterable<Tag> getAdditionalTags(Exchange ex) {
-        String faultCode = jaxwsFaultCodeProvider.getFaultCode(ex);
+    public Iterable<Tag> getAdditionalTags(Exchange ex, boolean client) {
+        String faultCode = jaxwsFaultCodeProvider.getFaultCode(ex, client);
         return Tags.of(jaxwsTags.faultCode(faultCode));
     }
 }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizer.java b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizer.java
index 06ee60a..f8ee30a 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizer.java
+++ b/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizer.java
@@ -37,8 +37,16 @@ public class JaxwsOperationTagsCustomizer implements TagsCustomizer {
     }
 
     @Override
-    public Iterable<Tag> getAdditionalTags(Exchange ex) {
-        Message request = ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+    public Iterable<Tag> getAdditionalTags(Exchange ex, boolean client) {
+        Message request = getRequest(ex, client);
         return Tags.of(jaxwsTags.operation(request));
     }
+    
+    private Message getRequest(Exchange ex, boolean client) {
+        if (client) {
+            return ofNullable(ex.getOutMessage()).orElseGet(ex::getOutFaultMessage);
+        } else {
+            return ofNullable(ex.getInMessage()).orElseGet(ex::getInFaultMessage);
+        }
+    }
 }
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContextTest.java
similarity index 92%
copy from rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java
copy to rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContextTest.java
index c4daf07..b53097e 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerClientMetricsContextTest.java
@@ -56,9 +56,9 @@ import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
-public class MicrometerMetricsContextTest {
+public class MicrometerClientMetricsContextTest {
 
     private static final long DUMMY_LONG = 0L;
     private static final Tag DEFAULT_DUMMY_TAG = Tag.of("defaultDummyKey", "dummyValue");
@@ -102,17 +102,17 @@ public class MicrometerMetricsContextTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
 
-        doReturn(request).when(exchange).getInMessage();
-        doReturn(singletonList(DEFAULT_DUMMY_TAG)).when(tagsProvider).getTags(exchange);
-        doReturn(singletonList(FIRST_ADDITIONAL_DUMMY_TAG)).when(firstTagsCustomizer).getAdditionalTags(exchange);
-        doReturn(singletonList(SECOND_ADDITIONAL_DUMMY_TAG)).when(secondTagsCustomizer).getAdditionalTags(exchange);
+        doReturn(request).when(exchange).getOutMessage();
+        doReturn(singletonList(DEFAULT_DUMMY_TAG)).when(tagsProvider).getTags(exchange, true);
+        doReturn(singletonList(FIRST_ADDITIONAL_DUMMY_TAG)).when(firstTagsCustomizer)
+            .getAdditionalTags(exchange, true);
+        doReturn(singletonList(SECOND_ADDITIONAL_DUMMY_TAG)).when(secondTagsCustomizer)
+            .getAdditionalTags(exchange, true);
 
-        underTest =
-                new MicrometerMetricsContext(
-                        registry, tagsProvider, timedAnnotationProvider,
-                        asList(firstTagsCustomizer, secondTagsCustomizer), DUMMY_METRIC, true);
+        underTest = new MicrometerClientMetricsContext(registry, tagsProvider, timedAnnotationProvider,
+            asList(firstTagsCustomizer, secondTagsCustomizer), DUMMY_METRIC);
     }
 
     @Test
@@ -164,7 +164,7 @@ public class MicrometerMetricsContextTest {
     public void testStopShouldCallStopOnAllTimedAnnotations() {
         // given
         doReturn(new HashSet<>(asList(firstTimedAnnotation, secondTimedAnnotation)))
-                .when(timedAnnotationProvider).getTimedAnnotations(exchange);
+                .when(timedAnnotationProvider).getTimedAnnotations(exchange, true);
 
         doReturn(FIRST_TIMED_ANNOTATION_DUMMY_VALUE).when(firstTimedAnnotation).value();
         doReturn("").when(firstTimedAnnotation).description();
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProviderTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProviderTest.java
index a79232a..554ff4a 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProviderTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsProviderTest.java
@@ -39,7 +39,7 @@ import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 public class MicrometerMetricsProviderTest {
 
@@ -62,10 +62,11 @@ public class MicrometerMetricsProviderTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
 
         micrometerMetricsProperties = new MicrometerMetricsProperties();
         micrometerMetricsProperties.setServerRequestsMetricName("http.server.requests");
+        micrometerMetricsProperties.setClientRequestsMetricName("http.client.requests");
         micrometerMetricsProperties.setAutoTimeRequests(true);
 
         underTest =
@@ -91,7 +92,7 @@ public class MicrometerMetricsProviderTest {
         MetricsContext actual = underTest.createOperationContext(endpoint, boi, false, "clientId");
 
         // then
-        assertThat(actual, instanceOf(MicrometerMetricsContext.class));
+        assertThat(actual, instanceOf(MicrometerServerMetricsContext.class));
         assertThat(getFieldValue(actual, "registry"), is(registry));
         assertThat(getFieldValue(actual, "tagsProvider"), is(tagsProvider));
         assertThat(getFieldValue(actual, "timedAnnotationProvider"), is(timedAnnotationProvider));
@@ -106,7 +107,13 @@ public class MicrometerMetricsProviderTest {
         MetricsContext actual = underTest.createOperationContext(endpoint, boi, true, "clientId");
 
         // then
-        assertThat(actual, is(nullValue()));
+        assertThat(actual, instanceOf(MicrometerClientMetricsContext.class));
+        assertThat(getFieldValue(actual, "registry"), is(registry));
+        assertThat(getFieldValue(actual, "tagsProvider"), is(tagsProvider));
+        assertThat(getFieldValue(actual, "timedAnnotationProvider"), is(timedAnnotationProvider));
+        assertThat(getFieldValue(actual, "metricName"), is("http.client.requests"));
+        assertThat(getFieldValue(actual, "autoTimeRequests"), is(true));
+        assertThat(getFieldValue(actual, "tagsCustomizers"), is(Collections.singletonList(tagsCustomizer)));
     }
     
     @Test
@@ -115,7 +122,7 @@ public class MicrometerMetricsProviderTest {
         MetricsContext actual = underTest.createResourceContext(endpoint, "resourceName", false, "clientId");
 
         // then
-        assertThat(actual, instanceOf(MicrometerMetricsContext.class));
+        assertThat(actual, instanceOf(MicrometerServerMetricsContext.class));
         assertThat(getFieldValue(actual, "registry"), is(registry));
         assertThat(getFieldValue(actual, "tagsProvider"), is(tagsProvider));
         assertThat(getFieldValue(actual, "timedAnnotationProvider"), is(timedAnnotationProvider));
@@ -125,16 +132,23 @@ public class MicrometerMetricsProviderTest {
     }
 
     @Test
-    public void testCreateClientResourceContext() {
+    public void testCreateClientResourceContext() throws NoSuchFieldException, IllegalAccessException {
         // when
         MetricsContext actual = underTest.createResourceContext(endpoint, "resourceName", true, "clientId");
 
         // then
-        assertThat(actual, is(nullValue()));
+        // then
+        assertThat(actual, instanceOf(MicrometerClientMetricsContext.class));
+        assertThat(getFieldValue(actual, "registry"), is(registry));
+        assertThat(getFieldValue(actual, "tagsProvider"), is(tagsProvider));
+        assertThat(getFieldValue(actual, "timedAnnotationProvider"), is(timedAnnotationProvider));
+        assertThat(getFieldValue(actual, "metricName"), is("http.client.requests"));
+        assertThat(getFieldValue(actual, "autoTimeRequests"), is(true));
+        assertThat(getFieldValue(actual, "tagsCustomizers"), is(Collections.singletonList(tagsCustomizer)));
     }
 
     private Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException {
-        Field field = object.getClass().getDeclaredField(fieldName);
+        Field field = object.getClass().getSuperclass().getDeclaredField(fieldName);
         field.setAccessible(true);
         return field.get(object);
     }
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContextTest.java
similarity index 95%
rename from rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java
rename to rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContextTest.java
index c4daf07..11fb975 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerMetricsContextTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/MicrometerServerMetricsContextTest.java
@@ -56,9 +56,9 @@ import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
-public class MicrometerMetricsContextTest {
+public class MicrometerServerMetricsContextTest {
 
     private static final long DUMMY_LONG = 0L;
     private static final Tag DEFAULT_DUMMY_TAG = Tag.of("defaultDummyKey", "dummyValue");
@@ -102,15 +102,17 @@ public class MicrometerMetricsContextTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
 
         doReturn(request).when(exchange).getInMessage();
-        doReturn(singletonList(DEFAULT_DUMMY_TAG)).when(tagsProvider).getTags(exchange);
-        doReturn(singletonList(FIRST_ADDITIONAL_DUMMY_TAG)).when(firstTagsCustomizer).getAdditionalTags(exchange);
-        doReturn(singletonList(SECOND_ADDITIONAL_DUMMY_TAG)).when(secondTagsCustomizer).getAdditionalTags(exchange);
+        doReturn(singletonList(DEFAULT_DUMMY_TAG)).when(tagsProvider).getTags(exchange, false);
+        doReturn(singletonList(FIRST_ADDITIONAL_DUMMY_TAG)).when(firstTagsCustomizer)
+            .getAdditionalTags(exchange, false);
+        doReturn(singletonList(SECOND_ADDITIONAL_DUMMY_TAG)).when(secondTagsCustomizer)
+            .getAdditionalTags(exchange, false);
 
         underTest =
-                new MicrometerMetricsContext(
+                new MicrometerServerMetricsContext(
                         registry, tagsProvider, timedAnnotationProvider,
                         asList(firstTagsCustomizer, secondTagsCustomizer), DUMMY_METRIC, true);
     }
@@ -164,7 +166,7 @@ public class MicrometerMetricsContextTest {
     public void testStopShouldCallStopOnAllTimedAnnotations() {
         // given
         doReturn(new HashSet<>(asList(firstTimedAnnotation, secondTimedAnnotation)))
-                .when(timedAnnotationProvider).getTimedAnnotations(exchange);
+                .when(timedAnnotationProvider).getTimedAnnotations(exchange, false);
 
         doReturn(FIRST_TIMED_ANNOTATION_DUMMY_VALUE).when(firstTimedAnnotation).value();
         doReturn("").when(firstTimedAnnotation).description();
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProviderTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProviderTest.java
index 54b7543..b99f79a 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProviderTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultExceptionClassProviderTest.java
@@ -63,7 +63,7 @@ public class DefaultExceptionClassProviderTest {
         doReturn(FAULT_EXCEPTION).when(exchange).get(Exception.class);
 
         // when
-        Class<?> actual = underTest.getExceptionClass(exchange);
+        Class<?> actual = underTest.getExceptionClass(exchange, false);
 
         // then
         assertThat(actual, equalTo(expected));
@@ -72,13 +72,13 @@ public class DefaultExceptionClassProviderTest {
     @Test
     public void testGetExceptionClassReturnCauseExceptionFromOutFaultMessage() {
         // given
-        Class expected = CauseException.class;
+        Class<?> expected = CauseException.class;
 
         doReturn(faultResponse).when(exchange).getOutFaultMessage();
         doReturn(FAULT_EXCEPTION).when(faultResponse).get(Exception.class);
 
         // when
-        Class<?> actual = underTest.getExceptionClass(exchange);
+        Class<?> actual = underTest.getExceptionClass(exchange, false);
 
         // then
         assertThat(actual, equalTo(expected));
@@ -87,13 +87,13 @@ public class DefaultExceptionClassProviderTest {
     @Test
     public void testGetExceptionClassReturnCauseExceptionFromInMessage() {
         // given
-        Class expected = CauseException.class;
+        Class<?> expected = CauseException.class;
 
         doReturn(request).when(exchange).getInMessage();
         doReturn(FAULT_EXCEPTION).when(request).get(Exception.class);
 
         // when
-        Class<?> actual = underTest.getExceptionClass(exchange);
+        Class<?> actual = underTest.getExceptionClass(exchange, false);
 
         // then
         assertThat(actual, equalTo(expected));
@@ -105,7 +105,7 @@ public class DefaultExceptionClassProviderTest {
         doReturn(new RuntimeException()).when(exchange).get(Exception.class);
 
         // when
-        Class<?> actual = underTest.getExceptionClass(exchange);
+        Class<?> actual = underTest.getExceptionClass(exchange, false);
 
         // then
         assertThat(actual, is(nullValue()));
@@ -116,12 +116,13 @@ public class DefaultExceptionClassProviderTest {
         // given
 
         // when
-        Class<?> actual = underTest.getExceptionClass(exchange);
+        Class<?> actual = underTest.getExceptionClass(exchange, false);
 
         // then
         assertThat(actual, is(nullValue()));
     }
 
     private static class CauseException extends RuntimeException {
+        private static final long serialVersionUID = 5321136931639340427L;
     }
 }
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProviderTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProviderTest.java
index 0229112..64d4716 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProviderTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/DefaultTimedAnnotationProviderTest.java
@@ -83,7 +83,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -103,7 +103,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -122,7 +122,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), containsInAnyOrder("customTimed"));
@@ -141,7 +141,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream()
@@ -162,7 +162,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), containsInAnyOrder("timed2"));
@@ -183,7 +183,7 @@ public class DefaultTimedAnnotationProviderTest {
         mockClass(TesterClass.class);
 
         // when
-        Set<Timed> actual = underTest.getTimedAnnotations(exchange);
+        Set<Timed> actual = underTest.getTimedAnnotations(exchange, false);
 
         // then
         assertThat(actual.stream().map(Timed::value).collect(Collectors.toSet()), empty());
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProviderTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProviderTest.java
index a6e5529..8434fae 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProviderTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/StandardTagsProviderTest.java
@@ -64,7 +64,7 @@ public class StandardTagsProviderTest {
         doReturn(uriTag).when(standardTags).uri(request);
 
         Tag exceptionTag = new ImmutableTag("exception", "exception");
-        doReturn(RuntimeException.class).when(exceptionClassProvider).getExceptionClass(exchange);
+        doReturn(RuntimeException.class).when(exceptionClassProvider).getExceptionClass(exchange, false);
         doReturn(exceptionTag).when(standardTags).exception(RuntimeException.class);
 
         Tag statusTag = new ImmutableTag("status", "status");
@@ -83,7 +83,7 @@ public class StandardTagsProviderTest {
         doReturn(response).when(exchange).getOutMessage();
 
         // when
-        Iterable<Tag> actual = underTest.getTags(exchange);
+        Iterable<Tag> actual = underTest.getTags(exchange, false);
 
         // then
         assertThat(actual, equalTo(expectedTags));
@@ -97,7 +97,7 @@ public class StandardTagsProviderTest {
         doReturn(response).when(exchange).getOutMessage();
 
         // when
-        Iterable<Tag> actual = underTest.getTags(exchange);
+        Iterable<Tag> actual = underTest.getTags(exchange, false);
 
         // then
         assertThat(actual, equalTo(expectedTags));
@@ -111,7 +111,7 @@ public class StandardTagsProviderTest {
         doReturn(response).when(exchange).getOutFaultMessage();
 
         // when
-        Iterable<Tag> actual = underTest.getTags(exchange);
+        Iterable<Tag> actual = underTest.getTags(exchange, false);
 
         // then
         assertThat(actual, equalTo(expectedTags));
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizerTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizerTest.java
index ff4fcef..36d64e1 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizerTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxrs/JaxrsOperationTagsCustomizerTest.java
@@ -55,14 +55,14 @@ public class JaxrsOperationTagsCustomizerTest {
     
     @Test
     public void testOperationReturnWithUnknownWhenRequestIsNull() {
-        final Iterable<Tag> actual = tagsCustomizer.getAdditionalTags(exchange);
+        final Iterable<Tag> actual = tagsCustomizer.getAdditionalTags(exchange, false);
         assertThat(actual, equalTo(Tags.of(Tag.of(OPERATION_METRIC_NAME, "UNKNOWN"))));
     }
 
     @Test
     public void testOperationReturnWithCorrectValue() throws NoSuchMethodException, SecurityException {
         message.put("org.apache.cxf.resource.method", getClass().getDeclaredMethod("getOperator"));
-        final Iterable<Tag> actual = tagsCustomizer.getAdditionalTags(exchange);
+        final Iterable<Tag> actual = tagsCustomizer.getAdditionalTags(exchange, false);
         assertThat(actual, equalTo(Tags.of(Tag.of(OPERATION_METRIC_NAME, DUMMY_OPERATOR))));
     }
     
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProviderTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProviderTest.java
index ada0abb..0dae669 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProviderTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProviderTest.java
@@ -33,7 +33,7 @@ import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 public class JaxwsFaultCodeProviderTest {
 
@@ -48,7 +48,7 @@ public class JaxwsFaultCodeProviderTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
         underTest = new JaxwsFaultCodeProvider();
     }
 
@@ -58,7 +58,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(RUNTIME_FAULT).when(ex).get(FaultMode.class);
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, equalTo(RUNTIME_FAULT_STRING));
@@ -72,7 +72,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(RUNTIME_FAULT).when(message).get(FaultMode.class);
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, equalTo(RUNTIME_FAULT_STRING));
@@ -85,7 +85,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(message).when(ex).getOutFaultMessage();
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, is(nullValue()));
@@ -100,7 +100,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(RUNTIME_FAULT).when(message).get(FaultMode.class);
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, equalTo(RUNTIME_FAULT_STRING));
@@ -114,7 +114,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(message).when(ex).getInMessage();
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, is(nullValue()));
@@ -128,7 +128,7 @@ public class JaxwsFaultCodeProviderTest {
         doReturn(null).when(ex).getInMessage();
 
         // when
-        String actual = underTest.getFaultCode(ex);
+        String actual = underTest.getFaultCode(ex, false);
 
         // then
         assertThat(actual, is(nullValue()));
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizerTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizerTest.java
index b0faed8..4e72fed 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizerTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeTagsCustomizerTest.java
@@ -32,7 +32,7 @@ import org.mockito.Mock;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 public class JaxwsFaultCodeTagsCustomizerTest {
 
@@ -51,18 +51,18 @@ public class JaxwsFaultCodeTagsCustomizerTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
         underTest = new JaxwsFaultCodeTagsCustomizer(jaxwsTags, jaxwsFaultCodeProvider);
     }
 
     @Test
     public void testAdditionalTagsShouldReturnFaultCodeAsTags() {
         // given
-        doReturn(DUMMY_FAULT_CODE).when(jaxwsFaultCodeProvider).getFaultCode(ex);
+        doReturn(DUMMY_FAULT_CODE).when(jaxwsFaultCodeProvider).getFaultCode(ex, false);
         doReturn(DUMMY_TAG).when(jaxwsTags).faultCode(DUMMY_FAULT_CODE);
 
         // when
-        Iterable<Tag> actual = underTest.getAdditionalTags(ex);
+        Iterable<Tag> actual = underTest.getAdditionalTags(ex, false);
 
         // then
         assertThat(actual, equalTo(Tags.of(DUMMY_TAG)));
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizerTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizerTest.java
index 804c8f6..8f01d14 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizerTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsOperationTagsCustomizerTest.java
@@ -33,7 +33,7 @@ import org.mockito.Mock;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 public class JaxwsOperationTagsCustomizerTest {
 
@@ -51,7 +51,7 @@ public class JaxwsOperationTagsCustomizerTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
         underTest = new JaxwsOperationTagsCustomizer(jaxwsTags);
     }
 
@@ -62,7 +62,7 @@ public class JaxwsOperationTagsCustomizerTest {
         doReturn(DUMMY_TAG).when(jaxwsTags).operation(request);
 
         // when
-        Iterable<Tag> actual = underTest.getAdditionalTags(ex);
+        Iterable<Tag> actual = underTest.getAdditionalTags(ex, false);
 
         // then
         assertThat(actual, equalTo(Tags.of(DUMMY_TAG)));
@@ -75,7 +75,7 @@ public class JaxwsOperationTagsCustomizerTest {
         doReturn(DUMMY_TAG).when(jaxwsTags).operation(request);
 
         // when
-        Iterable<Tag> actual = underTest.getAdditionalTags(ex);
+        Iterable<Tag> actual = underTest.getAdditionalTags(ex, false);
 
         // then
         assertThat(actual, equalTo(Tags.of(DUMMY_TAG)));
diff --git a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsTagsTest.java b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsTagsTest.java
index 726bbd5..3aab6ab 100644
--- a/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsTagsTest.java
+++ b/rt/features/metrics/src/test/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsTagsTest.java
@@ -34,7 +34,7 @@ import org.mockito.Mock;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.MockitoAnnotations.initMocks;
+import static org.mockito.MockitoAnnotations.openMocks;
 
 
 public class JaxwsTagsTest {
@@ -56,7 +56,7 @@ public class JaxwsTagsTest {
 
     @Before
     public void setUp() {
-        initMocks(this);
+        openMocks(this);
 
         doReturn(exchange).when(request).getExchange();
         doReturn(bindingOperationInfo).when(exchange).getBindingOperationInfo();
diff --git a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/Library.java b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/Library.java
index c540eb3..841599b 100644
--- a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/Library.java
+++ b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/Library.java
@@ -23,14 +23,7 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
 
-import javax.ws.rs.DELETE;
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
 import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
@@ -40,7 +33,7 @@ import io.swagger.v3.oas.annotations.Parameter;
 
 @Component
 @Path("library")
-public class Library {
+public class Library implements LibraryApi {
     private Map<String, Book> books = Collections.synchronizedMap(
         new TreeMap<String, Book>(String.CASE_INSENSITIVE_ORDER));
 
@@ -49,22 +42,19 @@ public class Library {
         books.put("2", new Book("Book #2", "Tom Tommyknocker"));
     }
 
-    @Produces({ MediaType.APPLICATION_JSON })
-    @GET
-    public Response getBooks(@Parameter(required = true) @QueryParam("page") @DefaultValue("1") int page) {
+    @Override
+    public Response getBooks(@Parameter(required = true) int page) {
         return Response.ok(books.values()).build();
     }
 
-    @Produces({ MediaType.APPLICATION_JSON })
-    @Path("{id}")
-    @GET
-    public Response getBook(@PathParam("id") String id) {
+    @Override
+    public Response getBook(String id) {
         return books.containsKey(id) 
             ? Response.ok().entity(books.get(id)).build() 
                 : Response.status(Status.NOT_FOUND).build();
     }
     
-    @DELETE
+    @Override
     public void deleteBooks() {
         throw new UnsupportedOperationException("Operation is not supported by the server");
     }
diff --git a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/LibraryApi.java
similarity index 56%
copy from rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java
copy to systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/LibraryApi.java
index 7128abd..b02a6e0 100644
--- a/rt/features/metrics/src/main/java/org/apache/cxf/metrics/micrometer/provider/jaxws/JaxwsFaultCodeProvider.java
+++ b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/resources/LibraryApi.java
@@ -17,24 +17,28 @@
  * under the License.
  */
 
-package org.apache.cxf.metrics.micrometer.provider.jaxws;
+package org.apache.cxf.systest.jaxrs.resources;
 
-import org.apache.cxf.message.Exchange;
-import org.apache.cxf.message.FaultMode;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
 
-public class JaxwsFaultCodeProvider {
+public interface LibraryApi {
+    @Produces({ MediaType.APPLICATION_JSON })
+    @GET
+    Response getBooks(@QueryParam("page") @DefaultValue("1") int page);
+
+    @Produces({ MediaType.APPLICATION_JSON })
+    @Path("{id}")
+    @GET
+    Response getBook(@PathParam("id") String id);
     
-    public String getFaultCode(Exchange ex) {
-        FaultMode fm = ex.get(FaultMode.class);
-        if (fm == null && ex.getOutFaultMessage() != null) {
-            fm = ex.getOutFaultMessage().get(FaultMode.class);
-        }
-        if (fm == null && ex.getInMessage() != null) {
-            fm = ex.getInMessage().get(FaultMode.class);
-        }
-        if (fm == null) {
-            return null;
-        }
-        return fm.name();
-    }
+    @DELETE
+    void deleteBooks();
 }
diff --git a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/spring/boot/SpringJaxrsTest.java b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/spring/boot/SpringJaxrsTest.java
index 7771653..67aa150 100644
--- a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/spring/boot/SpringJaxrsTest.java
+++ b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxrs/spring/boot/SpringJaxrsTest.java
@@ -19,10 +19,12 @@
 
 package org.apache.cxf.systest.jaxrs.spring.boot;
 
+import java.util.Arrays;
 import java.util.Map;
 
 import javax.ws.rs.InternalServerErrorException;
 import javax.ws.rs.NotFoundException;
+import javax.ws.rs.ProcessingException;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.Response;
@@ -31,10 +33,12 @@ import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
 
 import org.apache.cxf.bus.spring.SpringBus;
 import org.apache.cxf.feature.Feature;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactoryBean;
 import org.apache.cxf.metrics.MetricsFeature;
 import org.apache.cxf.metrics.MetricsProvider;
 import org.apache.cxf.systest.jaxrs.resources.Book;
 import org.apache.cxf.systest.jaxrs.resources.Library;
+import org.apache.cxf.systest.jaxrs.resources.LibraryApi;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -46,6 +50,7 @@ import org.springframework.context.annotation.ComponentScan;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.SocketUtils;
 
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.Tag;
@@ -71,6 +76,9 @@ public class SpringJaxrsTest {
     @Autowired
     private MeterRegistry registry;
     
+    @Autowired
+    private MetricsProvider metricsProvider;
+    
     @LocalServerPort
     private int port;
 
@@ -110,19 +118,33 @@ public class SpringJaxrsTest {
             assertThat(r.getStatus()).isEqualTo(200);
         }
         
-        RequiredSearch requestMetrics = registry.get("cxf.server.requests");
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "getBooks"),
+                entry("uri", "/api/library"),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
 
-        Map<Object, Object> tags = requestMetrics.timer().getId().getTags().stream()
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
                 .collect(toMap(Tag::getKey, Tag::getValue));
 
-        assertThat(tags)
+        assertThat(clientTags)
             .containsOnly(
-                    entry("exception", "None"),
-                    entry("method", "GET"),
-                    entry("operation", "getBooks"),
-                    entry("uri", "/api/library"),
-                    entry("outcome", "SUCCESS"),
-                    entry("status", "200"));
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library"),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
     }
     
     @Test
@@ -133,19 +155,33 @@ public class SpringJaxrsTest {
             .isInstanceOf(NotFoundException.class)
             .hasMessageContaining("Not Found");
 
-        RequiredSearch requestMetrics = registry.get("cxf.server.requests");
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "getBook"),
+                entry("uri", "/api/library/100"),
+                entry("outcome", "CLIENT_ERROR"),
+                entry("status", "404"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
 
-        Map<Object, Object> tags = requestMetrics.timer().getId().getTags().stream()
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
                 .collect(toMap(Tag::getKey, Tag::getValue));
 
-        assertThat(tags)
-                .containsOnly(
-                        entry("exception", "None"),
-                        entry("method", "GET"),
-                        entry("operation", "getBook"),
-                        entry("uri", "/api/library/100"),
-                        entry("outcome", "CLIENT_ERROR"),
-                        entry("status", "404"));
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library/100"),
+                entry("outcome", "CLIENT_ERROR"),
+                entry("status", "404"));
     }
     
     @Test
@@ -156,25 +192,219 @@ public class SpringJaxrsTest {
             .isInstanceOf(InternalServerErrorException.class)
             .hasMessageContaining("Internal Server Error");
 
-        RequiredSearch requestMetrics = registry.get("cxf.server.requests");
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "UnsupportedOperationException"),
+                entry("method", "DELETE"),
+                entry("operation", "deleteBooks"),
+                entry("uri", "/api/library"),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "DELETE"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library"),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+    }
+    
+    @Test
+    public void testJaxrsClientExceptionMetric() {
+        final int fakePort = SocketUtils.findAvailableTcpPort();
+        
+        final WebTarget target = ClientBuilder
+            .newClient()
+            .register(new MetricsFeature(metricsProvider))
+            .target("http://localhost:" + fakePort + "/api/library");
+        
+        assertThatThrownBy(() -> target.request().delete(String.class))
+            .isInstanceOf(ProcessingException.class)
+            .hasMessageContaining("Connection refused");
+
+        // no server meters
+        assertThat(registry.getMeters())
+            .noneMatch(m -> m.getId().getName().equals("cxf.server.requests"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
 
-        Map<Object, Object> tags = requestMetrics.timer().getId().getTags().stream()
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
                 .collect(toMap(Tag::getKey, Tag::getValue));
 
-        assertThat(tags)
-                .containsOnly(
-                        entry("exception", "UnsupportedOperationException"),
-                        entry("method", "DELETE"),
-                        entry("operation", "deleteBooks"),
-                        entry("uri", "/api/library"),
-                        entry("outcome", "SERVER_ERROR"),
-                        entry("status", "500"));
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "DELETE"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + fakePort + "/api/library"),
+                entry("outcome", "UNKNOWN"),
+                entry("status", "UNKNOWN"));
     }
     
+    @Test
+    public void testJaxrsProxySuccessMetric() {
+        final LibraryApi api = createApi(port);
+        
+        try (Response r = api.getBooks(1)) {
+            assertThat(r.getStatus()).isEqualTo(200);
+        }
+        
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "getBooks"),
+                entry("uri", "/api/library"),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library"),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+    }
+    
+    @Test
+    public void testJaxrsProxyExceptionMetric() {
+        final LibraryApi api = createApi(port);
+        
+        assertThatThrownBy(() -> api.deleteBooks())
+            .isInstanceOf(InternalServerErrorException.class)
+            .hasMessageContaining("Internal Server Error");
+
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "UnsupportedOperationException"),
+                entry("method", "DELETE"),
+                entry("operation", "deleteBooks"),
+                entry("uri", "/api/library"),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "DELETE"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library"),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+    }
+    
+    @Test
+    public void testJaxrsProxyFailedMetric() {
+        final LibraryApi api = createApi(port);
+
+        try (Response r = api.getBook("100")) {
+            assertThat(r.getStatus()).isEqualTo(404);
+        }
+
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "getBook"),
+                entry("uri", "/api/library/100"),
+                entry("outcome", "CLIENT_ERROR"),
+                entry("status", "404"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "GET"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + port + "/api/library/100"),
+                entry("outcome", "CLIENT_ERROR"),
+                entry("status", "404"));
+    }
+    
+    @Test
+    public void testJaxrsProxyClientExceptionMetric() {
+        final int fakePort = SocketUtils.findAvailableTcpPort();
+        final LibraryApi api = createApi(fakePort);
+
+        assertThatThrownBy(() -> api.deleteBooks())
+            .isInstanceOf(ProcessingException.class)
+            .hasMessageContaining("Connection refused");
+
+        // no server meters
+        assertThat(registry.getMeters())
+            .noneMatch(m -> m.getId().getName().equals("cxf.server.requests"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("method", "DELETE"),
+                entry("operation", "UNKNOWN"),
+                entry("uri", "http://localhost:" + fakePort + "/api/library"),
+                entry("outcome", "UNKNOWN"),
+                entry("status", "UNKNOWN"));
+    }
+    
+    private LibraryApi createApi(int portToUse) {
+        final JAXRSClientFactoryBean factory = new JAXRSClientFactoryBean();
+        factory.setAddress("http://localhost:" + portToUse + "/api/library");
+        factory.setFeatures(Arrays.asList(new MetricsFeature(metricsProvider)));
+        factory.setResourceClass(LibraryApi.class);
+        return factory.create(LibraryApi.class);
+    }
+
     private WebTarget createWebTarget() {
         return ClientBuilder
             .newClient()
             .register(JacksonJsonProvider.class)
+            .register(new MetricsFeature(metricsProvider))
             .target("http://localhost:" + port + "/api/library");
     }
 
diff --git a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxws/spring/boot/SpringJaxwsTest.java b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxws/spring/boot/SpringJaxwsTest.java
index 7e7ca0b..3f6f480 100644
--- a/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxws/spring/boot/SpringJaxwsTest.java
+++ b/systests/spring-boot/src/test/java/org/apache/cxf/systest/jaxws/spring/boot/SpringJaxwsTest.java
@@ -22,6 +22,7 @@ package org.apache.cxf.systest.jaxws.spring.boot;
 import java.io.StringReader;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.Map;
 
 import javax.xml.namespace.QName;
@@ -31,13 +32,16 @@ import javax.xml.ws.Dispatch;
 import javax.xml.ws.Endpoint;
 import javax.xml.ws.Service;
 import javax.xml.ws.Service.Mode;
+import javax.xml.ws.WebServiceException;
 import javax.xml.ws.soap.SOAPFaultException;
 
 import org.apache.cxf.Bus;
 import org.apache.cxf.jaxws.EndpointImpl;
+import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
 import org.apache.cxf.metrics.MetricsFeature;
 import org.apache.cxf.metrics.MetricsProvider;
 import org.apache.cxf.staxutils.StaxUtils;
+import org.apache.cxf.systest.jaxws.resources.HelloService;
 import org.apache.cxf.systest.jaxws.resources.HelloServiceImpl;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -50,6 +54,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.test.context.ActiveProfiles;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.util.SocketUtils;
 
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.Tag;
@@ -62,6 +67,7 @@ import org.junit.runner.RunWith;
 
 import static java.util.stream.Collectors.toMap;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.catchThrowable;
 import static org.assertj.core.api.Assertions.entry;
 
@@ -83,7 +89,10 @@ public class SpringJaxwsTest {
     public OutputCaptureRule output = new OutputCaptureRule();
 
     @Autowired
-    MeterRegistry registry;
+    private MeterRegistry registry;
+    
+    @Autowired
+    private MetricsProvider metricsProvider;
 
     @LocalServerPort
     private int port;
@@ -143,20 +152,35 @@ public class SpringJaxwsTest {
                         + "<return>Hello, Elan</return>"
                         + "</ns2:sayHelloResponse>");
 
-        RequiredSearch requestMetrics = registry.get("cxf.server.requests");
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
 
-        Map<Object, Object> tags = requestMetrics.timer().getId().getTags().stream()
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "None"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
                 .collect(toMap(Tag::getKey, Tag::getValue));
 
-        assertThat(tags)
-                .containsOnly(
-                        entry("exception", "None"),
-                        entry("faultCode", "None"),
-                        entry("method", "POST"),
-                        entry("operation", "sayHello"),
-                        entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
-                        entry("outcome", "SUCCESS"),
-                        entry("status", "200"));
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "None"),
+                entry("method", "POST"),
+                entry("operation", "Invoke"),
+                entry("uri", "http://localhost:" + port + "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
     }
 
     @Test
@@ -169,24 +193,39 @@ public class SpringJaxwsTest {
 
         // then
         assertThat(throwable)
-                .isInstanceOf(SOAPFaultException.class)
-                .hasMessageContaining("Fault occurred while processing");
+            .isInstanceOf(SOAPFaultException.class)
+            .hasMessageContaining("Fault occurred while processing");
+
 
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
 
-        RequiredSearch requestMetrics = registry.get("cxf.server.requests");
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
 
-        Map<Object, Object> tags = requestMetrics.timer().getId().getTags().stream()
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "NullPointerException"),
+                entry("faultCode", "UNCHECKED_APPLICATION_FAULT"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
                 .collect(toMap(Tag::getKey, Tag::getValue));
 
-        assertThat(tags)
-                .containsOnly(
-                        entry("exception", "NullPointerException"),
-                        entry("faultCode", "UNCHECKED_APPLICATION_FAULT"),
-                        entry("method", "POST"),
-                        entry("operation", "sayHello"),
-                        entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
-                        entry("outcome", "SERVER_ERROR"),
-                        entry("status", "500"));
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "UNCHECKED_APPLICATION_FAULT"),
+                entry("method", "POST"),
+                entry("operation", "Invoke"),
+                entry("uri", "http://localhost:" + port + "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
     }
 
     @Test
@@ -215,13 +254,126 @@ public class SpringJaxwsTest {
         assertThat(this.output).doesNotContain("Reached the maximum number of URI tags " + "for 'cxf.server.requests'");
 
     }
+    
+    @Test
+    public void testJaxwsProxySuccessMetric() throws MalformedURLException {
+        final HelloService api = createApi(port, HELLO_SERVICE_NAME_V1); 
+        assertThat(api.sayHello("Elan")).isEqualTo("Hello, Elan");
+
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "None"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "None"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "http://localhost:" + port + "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SUCCESS"),
+                entry("status", "200"));
+    }
+    
+    @Test
+    public void testJaxwsProxyFailedMetric() {
+        final HelloService api = createApi(port, HELLO_SERVICE_NAME_V1); 
+
+        // then
+        assertThatThrownBy(() -> api.sayHello(null))
+            .isInstanceOf(SOAPFaultException.class)
+            .hasMessageContaining("Fault occurred while processing");
+
+        RequiredSearch serverRequestMetrics = registry.get("cxf.server.requests");
+
+        Map<Object, Object> serverTags = serverRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(serverTags)
+            .containsOnly(
+                entry("exception", "NullPointerException"),
+                entry("faultCode", "UNCHECKED_APPLICATION_FAULT"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "UNCHECKED_APPLICATION_FAULT"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "http://localhost:" + port + "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "SERVER_ERROR"),
+                entry("status", "500"));
+    }
+
+    @Test
+    public void testJaxwsProxyClientExceptionMetric() throws MalformedURLException {
+        final int fakePort = SocketUtils.findAvailableTcpPort();
+        final HelloService api = createApi(fakePort, HELLO_SERVICE_NAME_V1); 
+        
+        assertThatThrownBy(() -> api.sayHello("Elan"))
+            .isInstanceOf(WebServiceException.class)
+            .hasMessageContaining("Could not send Message");
+
+        // no server meters
+        assertThat(registry.getMeters())
+            .noneMatch(m -> m.getId().getName().equals("cxf.server.requests"));
+        
+        RequiredSearch clientRequestMetrics = registry.get("cxf.client.requests");
+
+        Map<Object, Object> clientTags = clientRequestMetrics.timer().getId().getTags().stream()
+                .collect(toMap(Tag::getKey, Tag::getValue));
+
+        assertThat(clientTags)
+            .containsOnly(
+                entry("exception", "None"),
+                entry("faultCode", "RUNTIME_FAULT"),
+                entry("method", "POST"),
+                entry("operation", "sayHello"),
+                entry("uri", "http://localhost:" + fakePort + "/Service/" + HELLO_SERVICE_NAME_V1),
+                entry("outcome", "UNKNOWN"),
+                entry("status", "UNKNOWN"));
+    }
+    
+    private HelloService createApi(final int portToUse, final String serviceName) {
+        final JaxWsProxyFactoryBean  factory = new JaxWsProxyFactoryBean();
+        factory.setServiceClass(HelloService.class);
+        factory.setFeatures(Arrays.asList(new MetricsFeature(metricsProvider)));
+        factory.setAddress("http://localhost:" + portToUse + "/Service/" + serviceName);
+        return factory.create(HelloService.class);
+    }
 
     private String sendSoapRequest(String requestBody, final String serviceName) throws MalformedURLException {
         String address = "http://localhost:" + port + "/Service/" + serviceName;
 
         StreamSource source = new StreamSource(new StringReader(requestBody));
         Service service = Service.create(new URL(address + "?wsdl"),
-                new QName("http://service.ws.sample/", "HelloService"));
+                new QName("http://service.ws.sample/", "HelloService"), new MetricsFeature(metricsProvider));
         Dispatch<Source> dispatch = service.createDispatch(new QName("http://service.ws.sample/", "HelloPort"),
                 Source.class, Mode.PAYLOAD);