Repository: Unleash/unleash-client-java
Pull request #165 (2022-07-22 13:54:26)

task: Add an OkHttpMetricsSender
So, having added an OkHttpFeatureFetcher in a previous PR, this PR adds a metricssender which uses OkHttp.

stale[bot]: This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

thomasheartman: @chriswk This PR has been marked as stale: do we still want to get it in or should we close it?
stale[bot]: This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

sonatype-lift[bot]: :warning: **1 God Class** was detected by Lift in this project. [Visit the Lift web console](https://lift.sonatype.com/results/github.com/Unleash/unleash-client-java/01GEPJEMFP4HW0YYSCMBZESN8X?tab=technical-debt&utm_source=github.com&utm_campaign=lift-comment&utm_content=Unleash\%20unleash-client-java) for more details.

Patch:
diff --git a/README.md b/README.md
index c1780d88..9479572e 100644
--- a/README.md
+++ b/README.md
@@ -230,7 +230,7 @@ If you want to use OkHttp instead of HttpURLConnection you'll need a dependency
 <dependency>
     <groupId>com.squareup.okhttp3</groupId>
     <artifactId>okhttp</artifactId>
-    <version>4.9+</version>
+    <version>4.10+</version>
 </dependency>
 ```
 
@@ -246,6 +246,32 @@ UnleashConfig config = UnleashConfig.builder()
 
 This will then start using OkHttp instead of HttpURLConnection.
 
+### Metrics sender
+The Unleash Java client now supports using your own metrics sender.
+The Config builder has been expanded to accept a `io.getunleash.util.MetricsSenderFactory` which should be a `Function<UnleashConfig, MetricsSender>`.
+
+If you want to use OkHttp instead of HttpURLConnection you'll need a dependency on okhttp
+
+```xml
+<dependency>
+    <groupId>com.squareup.okhttp3</groupId>
+    <artifactId>okhttp</artifactId>
+    <version>4.10+</version>
+</dependency>
+```
+
+Then you can change your config to
+```java
+UnleashConfig config = UnleashConfig.builder()
+    .appName("my-app")
+    .unleashAPI("http://unleash.org")
+    .customHttpHeader("Authorization", "API token")
+    .unleashMetricsSenderFactory(OkHttpMetricsSender::new)
+    .build();
+```
+
+This will then start using OkHttp instead of HttpURLConnection to send metrics
+
 ## Local backup
 By default unleash-client fetches the feature toggles from unleash-server every 10s, and stores the
 result in `unleash-repo.json` which is located in the `java.io.tmpdir` directory. This means that if
@@ -411,6 +437,8 @@ The `UnleashConfigBuilder` class (created via `UnleashConfig.builder()`) exposes
 | `toggleBootstrapProvider`                  | Add a [bootstrap provider](#bootstrapping) (must implement the `ToggleBootstrapProvider` interface)                                                                                                                                              | No       |                                                                                                                      |
 | `unleashAPI`                               | The URL of the Unleash API.                                                                                                                                                                                                                      | Yes      | `null`                                                                                                               |
 | `unleashContextProvider`                   | An [Unleash context provider used to configure Unleash](#2-via-an-unleashcontextprovider).                                                                                                                                                       | No       | `null`                                                                                                               |
+| `unleashFeatureFetcherFactory`             | A factory providing a FeatureFetcher implementation.                                                                                                                                                                                             | No       | [`HttpFeatureFetcher::new`](src/main/java/io/getunleash/repository/HttpFeatureFetcher.java)                          |
+| `unleashMetricsSenderFactory`              | A factory providing a MetricSender implementation.                                                                                                                                                                                               | No       | [`JavaHttpMetricsSender::new`](src/main/java/io/getunleash/metric/JavaHttpMetricsSender.java)                        |
 
 When you have set all the desired options, initialize the configuration with the `build` method.
 You can then pass the configuration to the Unleash client constructor.
diff --git a/src/main/java/io/getunleash/metric/UnleashMetricsSender.java b/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java
similarity index 80%
rename from src/main/java/io/getunleash/metric/UnleashMetricsSender.java
rename to src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java
index 0c8ab71d..8f6ecfcb 100644
--- a/src/main/java/io/getunleash/metric/UnleashMetricsSender.java
+++ b/src/main/java/io/getunleash/metric/DefaultHttpMetricsSender.java
@@ -1,22 +1,20 @@
 package io.getunleash.metric;
 
-import static java.time.format.DateTimeFormatter.ISO_INSTANT;
-
 import com.google.gson.*;
 import io.getunleash.UnleashException;
 import io.getunleash.event.EventDispatcher;
+import io.getunleash.util.AtomicLongSerializer;
+import io.getunleash.util.DateTimeSerializer;
 import io.getunleash.util.UnleashConfig;
 import io.getunleash.util.UnleashURLs;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
-import java.lang.reflect.Type;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.time.LocalDateTime;
-import java.time.ZoneOffset;
 import java.util.concurrent.atomic.AtomicLong;
 
-public class UnleashMetricsSender {
+public class DefaultHttpMetricsSender implements MetricSender {
     private static final int CONNECT_TIMEOUT = 1000;
 
     private final Gson gson;
@@ -25,7 +23,7 @@ public class UnleashMetricsSender {
     private final URL clientRegistrationURL;
     private final URL clientMetricsURL;
 
-    public UnleashMetricsSender(UnleashConfig unleashConfig) {
+    public DefaultHttpMetricsSender(UnleashConfig unleashConfig) {
         this.unleashConfig = unleashConfig;
         this.eventDispatcher = new EventDispatcher(unleashConfig);
         UnleashURLs urls = unleashConfig.getUnleashURLs();
@@ -39,25 +37,6 @@ public class UnleashMetricsSender {
                         .create();
     }
 
-    static class DateTimeSerializer implements JsonSerializer<LocalDateTime> {
-        @Override
-        public JsonElement serialize(
-                LocalDateTime localDateTime,
-                Type type,
-                JsonSerializationContext jsonSerializationContext) {
-            return new JsonPrimitive(ISO_INSTANT.format(localDateTime.toInstant(ZoneOffset.UTC)));
-        }
-    }
-
-    static class AtomicLongSerializer implements JsonSerializer<AtomicLong> {
-
-        @Override
-        public JsonElement serialize(
-                AtomicLong src, Type typeOfSrc, JsonSerializationContext context) {
-            return new JsonPrimitive(src.get());
-        }
-    }
-
     public void registerClient(ClientRegistration registration) {
         if (!unleashConfig.isDisableMetrics()) {
             try {
diff --git a/src/main/java/io/getunleash/metric/MetricSender.java b/src/main/java/io/getunleash/metric/MetricSender.java
new file mode 100644
index 00000000..221f2de8
--- /dev/null
+++ b/src/main/java/io/getunleash/metric/MetricSender.java
@@ -0,0 +1,7 @@
+package io.getunleash.metric;
+
+public interface MetricSender {
+    void registerClient(ClientRegistration registration);
+
+    void sendMetrics(ClientMetrics metrics);
+}
diff --git a/src/main/java/io/getunleash/metric/OkHttpMetricsSender.java b/src/main/java/io/getunleash/metric/OkHttpMetricsSender.java
new file mode 100644
index 00000000..b637ff2d
--- /dev/null
+++ b/src/main/java/io/getunleash/metric/OkHttpMetricsSender.java
@@ -0,0 +1,95 @@
+package io.getunleash.metric;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.getunleash.UnleashException;
+import io.getunleash.event.EventDispatcher;
+import io.getunleash.util.AtomicLongSerializer;
+import io.getunleash.util.DateTimeSerializer;
+import io.getunleash.util.OkHttpClientConfigurer;
+import io.getunleash.util.UnleashConfig;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+public class OkHttpMetricsSender implements MetricSender {
+    private final UnleashConfig config;
+    private final MediaType JSON =
+            Objects.requireNonNull(MediaType.parse("application/json; charset=utf-8"));
+
+    private final EventDispatcher eventDispatcher;
+    private final OkHttpClient client;
+
+    private final Gson gson;
+
+    private final HttpUrl clientRegistrationUrl;
+
+    private final HttpUrl clientMetricsUrl;
+
+    public OkHttpMetricsSender(UnleashConfig config) {
+        this.config = config;
+        this.clientMetricsUrl =
+                Objects.requireNonNull(HttpUrl.get(config.getUnleashURLs().getClientMetricsURL()));
+        this.clientRegistrationUrl =
+                Objects.requireNonNull(HttpUrl.get(config.getUnleashURLs().getClientRegisterURL()));
+        this.eventDispatcher = new EventDispatcher(config);
+
+        OkHttpClient.Builder builder;
+        if (config.getProxy() != null) {
+            builder = new OkHttpClient.Builder().proxy(config.getProxy());
+        } else {
+            builder = new OkHttpClient.Builder();
+        }
+        builder =
+                builder.callTimeout(config.getSendMetricsConnectTimeout())
+                        .readTimeout(config.getSendMetricsReadTimeout());
+        this.client = OkHttpClientConfigurer.configureInterceptor(config, builder.build());
+
+        this.gson =
+                new GsonBuilder()
+                        .registerTypeAdapter(LocalDateTime.class, new DateTimeSerializer())
+                        .registerTypeAdapter(AtomicLong.class, new AtomicLongSerializer())
+                        .create();
+    }
+
+    @Override
+    public void registerClient(ClientRegistration registration) {
+        if (!config.isDisableMetrics()) {
+            try {
+                post(clientRegistrationUrl, registration);
+                eventDispatcher.dispatch(registration);
+            } catch (UnleashException ex) {
+                eventDispatcher.dispatch(ex);
+            }
+        }
+    }
+
+    @Override
+    public void sendMetrics(ClientMetrics metrics) {
+        if (!config.isDisableMetrics()) {
+            try {
+                post(clientMetricsUrl, metrics);
+                eventDispatcher.dispatch(metrics);
+            } catch (UnleashException ex) {
+                eventDispatcher.dispatch(ex);
+            }
+        }
+    }
+
+    private int post(HttpUrl url, Object o) {
+        RequestBody body = RequestBody.create(gson.toJson(o), JSON);
+        Request request = new Request.Builder().url(url).post(body).build();
+        try (Response response = this.client.newCall(request).execute()) {
+            return response.code();
+        } catch (IOException ioEx) {
+            throw new UnleashException("Could not post to Unleash API", ioEx);
+        }
+    }
+}
diff --git a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java
index fa678707..83d4ae23 100644
--- a/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java
+++ b/src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java
@@ -9,27 +9,26 @@ import java.util.Set;
 public class UnleashMetricServiceImpl implements UnleashMetricService {
     private final LocalDateTime started;
     private final UnleashConfig unleashConfig;
-    private final long metricsInterval;
-    private final UnleashMetricsSender unleashMetricsSender;
+
+    private final MetricSender metricSender;
 
     // mutable
     private volatile MetricsBucket currentMetricsBucket;
 
     public UnleashMetricServiceImpl(
             UnleashConfig unleashConfig, UnleashScheduledExecutor executor) {
-        this(unleashConfig, new UnleashMetricsSender(unleashConfig), executor);
+        this(unleashConfig, unleashConfig.getMetricSenderFactory().apply(unleashConfig), executor);
     }
 
     public UnleashMetricServiceImpl(
             UnleashConfig unleashConfig,
-            UnleashMetricsSender unleashMetricsSender,
+            MetricSender metricSender,
             UnleashScheduledExecutor executor) {
         this.currentMetricsBucket = new MetricsBucket();
         this.started = LocalDateTime.now(ZoneId.of("UTC"));
         this.unleashConfig = unleashConfig;
-        this.metricsInterval = unleashConfig.getSendMetricsInterval();
-        this.unleashMetricsSender = unleashMetricsSender;
-
+        this.metricSender = metricSender;
+        long metricsInterval = unleashConfig.getSendMetricsInterval();
         executor.setInterval(sendMetrics(), metricsInterval, metricsInterval);
     }
 
@@ -37,7 +36,7 @@ public class UnleashMetricServiceImpl implements UnleashMetricService {
     public void register(Set<String> strategies) {
         ClientRegistration registration =
                 new ClientRegistration(unleashConfig, started, strategies);
-        unleashMetricsSender.registerClient(registration);
+        metricSender.registerClient(registration);
     }
 
     @Override
@@ -56,7 +55,7 @@ public class UnleashMetricServiceImpl implements UnleashMetricService {
             this.currentMetricsBucket = new MetricsBucket();
             metricsBucket.end();
             ClientMetrics metrics = new ClientMetrics(unleashConfig, metricsBucket);
-            unleashMetricsSender.sendMetrics(metrics);
+            metricSender.sendMetrics(metrics);
         };
     }
 }
diff --git a/src/main/java/io/getunleash/repository/OkHttpFeatureFetcher.java b/src/main/java/io/getunleash/repository/OkHttpFeatureFetcher.java
index 60767c77..8f777370 100644
--- a/src/main/java/io/getunleash/repository/OkHttpFeatureFetcher.java
+++ b/src/main/java/io/getunleash/repository/OkHttpFeatureFetcher.java
@@ -1,15 +1,12 @@
 package io.getunleash.repository;
 
-import static io.getunleash.util.UnleashConfig.UNLEASH_APP_NAME_HEADER;
-import static io.getunleash.util.UnleashConfig.UNLEASH_INSTANCE_ID_HEADER;
-
 import com.google.gson.JsonSyntaxException;
 import io.getunleash.UnleashException;
+import io.getunleash.util.OkHttpClientConfigurer;
 import io.getunleash.util.UnleashConfig;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.util.Map;
 import java.util.Objects;
 import okhttp3.Cache;
 import okhttp3.HttpUrl;
@@ -47,11 +44,11 @@ public class OkHttpFeatureFetcher implements FeatureFetcher {
                                         .getFetchTogglesURL(
                                                 unleashConfig.getProjectName(),
                                                 unleashConfig.getNamePrefix())));
-        this.client = configureInterceptor(unleashConfig, builder.build());
+        this.client = OkHttpClientConfigurer.configureInterceptor(unleashConfig, builder.build());
     }
 
     public OkHttpFeatureFetcher(UnleashConfig unleashConfig, OkHttpClient client) {
-        this.client = configureInterceptor(unleashConfig, client);
+        this.client = OkHttpClientConfigurer.configureInterceptor(unleashConfig, client);
         this.toggleUrl =
                 Objects.requireNonNull(
                         HttpUrl.get(
@@ -62,42 +59,6 @@ public class OkHttpFeatureFetcher implements FeatureFetcher {
                                                 unleashConfig.getNamePrefix())));
     }
 
-    public OkHttpClient configureInterceptor(UnleashConfig config, OkHttpClient client) {
-        return client.newBuilder()
-                .addInterceptor(
-                        (c) -> {
-                            Request.Builder headers =
-                                    c.request()
-                                            .newBuilder()
-                                            .addHeader("Content-Type", "application/json")
-                                            .addHeader("Accept", "application/json")
-                                            .addHeader(UNLEASH_APP_NAME_HEADER, config.getAppName())
-                                            .addHeader(
-                                                    UNLEASH_INSTANCE_ID_HEADER,
-                                                    config.getInstanceId())
-                                            .addHeader("User-Agent", config.getAppName())
-                                            .addHeader(
-                                                    "Unleash-Client-Spec",
-                                                    config.getClientSpecificationVersion());
-                            for (Map.Entry<String, String> headerEntry :
-                                    config.getCustomHttpHeaders().entrySet()) {
-                                headers =
-                                        headers.addHeader(
-                                                headerEntry.getKey(), headerEntry.getValue());
-                            }
-                            for (Map.Entry<String, String> headerEntry :
-                                    config.getCustomHttpHeadersProvider()
-                                            .getCustomHeaders()
-                                            .entrySet()) {
-                                headers =
-                                        headers.addHeader(
-                                                headerEntry.getKey(), headerEntry.getValue());
-                            }
-                            return c.proceed(headers.build());
-                        })
-                .build();
-    }
-
     @Override
     public ClientFeaturesResponse fetchFeatures() throws UnleashException {
         Request request = new Request.Builder().url(toggleUrl).get().build();
diff --git a/src/main/java/io/getunleash/util/AtomicLongSerializer.java b/src/main/java/io/getunleash/util/AtomicLongSerializer.java
new file mode 100644
index 00000000..54117f36
--- /dev/null
+++ b/src/main/java/io/getunleash/util/AtomicLongSerializer.java
@@ -0,0 +1,16 @@
+package io.getunleash.util;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class AtomicLongSerializer implements JsonSerializer<AtomicLong> {
+
+    @Override
+    public JsonElement serialize(AtomicLong src, Type typeOfSrc, JsonSerializationContext context) {
+        return new JsonPrimitive(src.get());
+    }
+}
diff --git a/src/main/java/io/getunleash/util/DateTimeSerializer.java b/src/main/java/io/getunleash/util/DateTimeSerializer.java
new file mode 100644
index 00000000..1a40e885
--- /dev/null
+++ b/src/main/java/io/getunleash/util/DateTimeSerializer.java
@@ -0,0 +1,21 @@
+package io.getunleash.util;
+
+import static java.time.format.DateTimeFormatter.ISO_INSTANT;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+public class DateTimeSerializer implements JsonSerializer<LocalDateTime> {
+    @Override
+    public JsonElement serialize(
+            LocalDateTime localDateTime,
+            Type type,
+            JsonSerializationContext jsonSerializationContext) {
+        return new JsonPrimitive(ISO_INSTANT.format(localDateTime.toInstant(ZoneOffset.UTC)));
+    }
+}
diff --git a/src/main/java/io/getunleash/util/MetricSenderFactory.java b/src/main/java/io/getunleash/util/MetricSenderFactory.java
new file mode 100644
index 00000000..e6ab639f
--- /dev/null
+++ b/src/main/java/io/getunleash/util/MetricSenderFactory.java
@@ -0,0 +1,6 @@
+package io.getunleash.util;
+
+import io.getunleash.metric.MetricSender;
+import java.util.function.Function;
+
+public interface MetricSenderFactory extends Function<UnleashConfig, MetricSender> {}
diff --git a/src/main/java/io/getunleash/util/OkHttpClientConfigurer.java b/src/main/java/io/getunleash/util/OkHttpClientConfigurer.java
new file mode 100644
index 00000000..afcc6188
--- /dev/null
+++ b/src/main/java/io/getunleash/util/OkHttpClientConfigurer.java
@@ -0,0 +1,46 @@
+package io.getunleash.util;
+
+import static io.getunleash.util.UnleashConfig.UNLEASH_APP_NAME_HEADER;
+import static io.getunleash.util.UnleashConfig.UNLEASH_INSTANCE_ID_HEADER;
+
+import java.util.Map;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
+public class OkHttpClientConfigurer {
+    public static OkHttpClient configureInterceptor(UnleashConfig config, OkHttpClient client) {
+        return client.newBuilder()
+                .addInterceptor(
+                        (c) -> {
+                            Request.Builder headers =
+                                    c.request()
+                                            .newBuilder()
+                                            .addHeader("Content-Type", "application/json")
+                                            .addHeader("Accept", "application/json")
+                                            .addHeader(UNLEASH_APP_NAME_HEADER, config.getAppName())
+                                            .addHeader(
+                                                    UNLEASH_INSTANCE_ID_HEADER,
+                                                    config.getInstanceId())
+                                            .addHeader("User-Agent", config.getAppName())
+                                            .addHeader(
+                                                    "Unleash-Client-Spec",
+                                                    config.getClientSpecificationVersion());
+                            for (Map.Entry<String, String> headerEntry :
+                                    config.getCustomHttpHeaders().entrySet()) {
+                                headers =
+                                        headers.addHeader(
+                                                headerEntry.getKey(), headerEntry.getValue());
+                            }
+                            for (Map.Entry<String, String> headerEntry :
+                                    config.getCustomHttpHeadersProvider()
+                                            .getCustomHeaders()
+                                            .entrySet()) {
+                                headers =
+                                        headers.addHeader(
+                                                headerEntry.getKey(), headerEntry.getValue());
+                            }
+                            return c.proceed(headers.build());
+                        })
+                .build();
+    }
+}
diff --git a/src/main/java/io/getunleash/util/UnleashConfig.java b/src/main/java/io/getunleash/util/UnleashConfig.java
index 6a4c9803..7f991370 100644
--- a/src/main/java/io/getunleash/util/UnleashConfig.java
+++ b/src/main/java/io/getunleash/util/UnleashConfig.java
@@ -8,9 +8,11 @@ import io.getunleash.UnleashContextProvider;
 import io.getunleash.event.NoOpSubscriber;
 import io.getunleash.event.UnleashSubscriber;
 import io.getunleash.lang.Nullable;
+import io.getunleash.metric.DefaultHttpMetricsSender;
 import io.getunleash.repository.HttpFeatureFetcher;
 import io.getunleash.repository.ToggleBootstrapProvider;
 import io.getunleash.strategy.Strategy;
+
 import java.io.File;
 import java.net.Authenticator;
 import java.net.HttpURLConnection;
@@ -58,6 +60,9 @@ public class UnleashConfig {
     private final boolean disableMetrics;
     private final boolean isProxyAuthenticationByJvmProperties;
     private final UnleashFeatureFetcherFactory unleashFeatureFetcherFactory;
+
+    private final MetricSenderFactory metricSenderFactory;
+
     private final UnleashContextProvider contextProvider;
     private final boolean synchronousFetchOnInitialisation;
     private final UnleashScheduledExecutor unleashScheduledExecutor;
@@ -89,6 +94,7 @@ public class UnleashConfig {
             boolean isProxyAuthenticationByJvmProperties,
             boolean synchronousFetchOnInitialisation,
             UnleashFeatureFetcherFactory unleashFeatureFetcherFactory,
+            MetricSenderFactory metricSenderFactory,
             @Nullable UnleashScheduledExecutor unleashScheduledExecutor,
             @Nullable UnleashSubscriber unleashSubscriber,
             @Nullable Strategy fallbackStrategy,
@@ -155,6 +161,7 @@ public class UnleashConfig {
         this.toggleBootstrapProvider = unleashBootstrapProvider;
         this.proxy = proxy;
         this.unleashFeatureFetcherFactory = unleashFeatureFetcherFactory;
+        this.metricSenderFactory = metricSenderFactory;
         this.clientSpecificationVersion =
                 UnleashProperties.getProperty("client.specification.version");
     }
@@ -295,6 +302,10 @@ public class UnleashConfig {
         return proxy;
     }
 
+    public MetricSenderFactory getMetricSenderFactory() {
+        return this.metricSenderFactory;
+    }
+
     public UnleashFeatureFetcherFactory getUnleashFeatureFetcherFactory() {
         return this.unleashFeatureFetcherFactory;
     }
@@ -379,6 +390,8 @@ public class UnleashConfig {
         private Duration sendMetricsReadTimeout = Duration.ofSeconds(10);
         private boolean disableMetrics = false;
         private UnleashFeatureFetcherFactory unleashFeatureFetcherFactory = HttpFeatureFetcher::new;
+
+        private MetricSenderFactory unleashMetricSenderFactory = DefaultHttpMetricsSender::new;
         private UnleashContextProvider contextProvider =
                 UnleashContextProvider.getDefaultProvider();
         private boolean synchronousFetchOnInitialisation = false;
@@ -456,6 +469,11 @@ public class UnleashConfig {
             return this;
         }
 
+        public Builder metricsSenderFactory(MetricSenderFactory metricSenderFactory) {
+            this.unleashMetricSenderFactory = metricSenderFactory;
+            return this;
+        }
+
         public Builder fetchTogglesInterval(long fetchTogglesInterval) {
             this.fetchTogglesInterval = fetchTogglesInterval;
             return this;
@@ -625,6 +643,7 @@ public class UnleashConfig {
                     isProxyAuthenticationByJvmProperties,
                     synchronousFetchOnInitialisation,
                     unleashFeatureFetcherFactory,
+                    unleashMetricSenderFactory,
                     Optional.ofNullable(scheduledExecutor)
                             .orElseGet(UnleashScheduledExecutorImpl::getInstance),
                     Optional.ofNullable(unleashSubscriber).orElseGet(NoOpSubscriber::new),


Test patch:
diff --git a/src/test/java/io/getunleash/metric/UnleashMetricsSenderTest.java b/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java
similarity index 93%
rename from src/test/java/io/getunleash/metric/UnleashMetricsSenderTest.java
rename to src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java
index 7e1d9185..f4250a99 100644
--- a/src/test/java/io/getunleash/metric/UnleashMetricsSenderTest.java
+++ b/src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java
@@ -19,7 +19,7 @@ import java.util.HashSet;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
-public class UnleashMetricsSenderTest {
+public class DefaultHttpMetricsSenderTest {
 
     @RegisterExtension
     static WireMockExtension serverMock =
@@ -38,7 +38,7 @@ public class UnleashMetricsSenderTest {
         URI uri = new URI("http://localhost:" + serverMock.getPort());
         UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
 
-        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config);
         sender.registerClient(
                 new ClientRegistration(config, LocalDateTime.now(), new HashSet<String>()));
 
@@ -59,7 +59,7 @@ public class UnleashMetricsSenderTest {
         URI uri = new URI("http://localhost:" + serverMock.getPort());
         UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
 
-        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config);
         MetricsBucket bucket = new MetricsBucket();
         ClientMetrics metrics = new ClientMetrics(config, bucket);
         sender.sendMetrics(metrics);
@@ -81,7 +81,7 @@ public class UnleashMetricsSenderTest {
         URI uri = new URI("http://localhost:" + serverMock.getPort());
         UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
 
-        UnleashMetricsSender sender = new UnleashMetricsSender(config);
+        DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config);
         MetricsBucket bucket = new MetricsBucket();
         ClientMetrics metrics = new ClientMetrics(config, bucket);
         sender.sendMetrics(metrics);
diff --git a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java
index 811dc6a3..07939f45 100644
--- a/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java
+++ b/src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java
@@ -38,7 +38,7 @@ public class UnleashMetricServiceImplTest {
                         .build();
 
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
-        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+        DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
 
         UnleashMetricService unleashMetricService =
                 new UnleashMetricServiceImpl(config, sender, executor);
@@ -70,7 +70,7 @@ public class UnleashMetricServiceImplTest {
                         .build();
 
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
-        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+        DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
 
         UnleashMetricService unleashMetricService =
                 new UnleashMetricServiceImpl(config, sender, executor);
@@ -96,7 +96,7 @@ public class UnleashMetricServiceImplTest {
                         .build();
 
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
-        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+        DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
 
         UnleashMetricService unleashMetricService =
                 new UnleashMetricServiceImpl(config, sender, executor);
@@ -119,7 +119,7 @@ public class UnleashMetricServiceImplTest {
                         .build();
 
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
-        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+        DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
 
         UnleashMetricService unleashMetricService =
                 new UnleashMetricServiceImpl(config, sender, executor);
@@ -160,7 +160,7 @@ public class UnleashMetricServiceImplTest {
                         .build();
 
         UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
-        UnleashMetricsSender sender = mock(UnleashMetricsSender.class);
+        DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
 
         UnleashMetricService unleashMetricService =
                 new UnleashMetricServiceImpl(config, sender, executor);