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:59 UTC

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

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);