Skip to content

Commit 8c0f334

Browse files
authored
feat: start sending impact metrics in payload (#341)
1 parent bad6723 commit 8c0f334

5 files changed

Lines changed: 192 additions & 64 deletions

File tree

src/main/java/io/getunleash/metric/UnleashMetricServiceImpl.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import io.getunleash.engine.MetricsBucket;
44
import io.getunleash.engine.UnleashEngine;
5+
import io.getunleash.impactmetrics.CollectedMetric;
6+
import io.getunleash.impactmetrics.ImpactMetricsDataSource;
57
import io.getunleash.util.Throttler;
68
import io.getunleash.util.UnleashConfig;
79
import io.getunleash.util.UnleashScheduledExecutor;
810
import java.time.LocalDateTime;
911
import java.time.ZoneId;
12+
import java.util.List;
1013
import java.util.Set;
1114

1215
public class UnleashMetricServiceImpl implements UnleashMetricService {
@@ -19,6 +22,8 @@ public class UnleashMetricServiceImpl implements UnleashMetricService {
1922

2023
private final Throttler throttler;
2124

25+
private final ImpactMetricsDataSource impactMetricsRegistry;
26+
2227
public UnleashMetricServiceImpl(
2328
UnleashConfig unleashConfig, UnleashScheduledExecutor executor, UnleashEngine engine) {
2429
this(
@@ -42,6 +47,7 @@ public UnleashMetricServiceImpl(
4247
300,
4348
unleashConfig.getUnleashURLs().getClientMetricsURL());
4449
this.engine = engine;
50+
this.impactMetricsRegistry = unleashConfig.getImpactMetricsRegistry();
4551
long metricsInterval = unleashConfig.getSendMetricsInterval();
4652

4753
executor.setInterval(sendMetrics(), metricsInterval, metricsInterval);
@@ -59,13 +65,21 @@ private Runnable sendMetrics() {
5965
if (throttler.performAction()) {
6066
MetricsBucket bucket = this.engine.getMetrics();
6167

62-
ClientMetrics metrics = new ClientMetrics(unleashConfig, bucket);
68+
List<CollectedMetric> impactMetrics = impactMetricsRegistry.collect();
69+
List<CollectedMetric> impactMetricsOrNull =
70+
impactMetrics.isEmpty() ? null : impactMetrics;
71+
72+
ClientMetrics metrics =
73+
new ClientMetrics(unleashConfig, bucket, impactMetricsOrNull);
6374
int statusCode = metricSender.sendMetrics(metrics);
6475
if (statusCode >= 200 && statusCode < 400) {
6576
throttler.decrementFailureCountAndResetSkips();
6677
}
6778
if (statusCode >= 400) {
6879
throttler.handleHttpErrorCodes(statusCode);
80+
if (impactMetricsOrNull != null) {
81+
impactMetricsRegistry.restore(impactMetricsOrNull);
82+
}
6983
}
7084

7185
} else {

src/main/java/io/getunleash/util/UnleashConfig.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.getunleash.event.NoOpSubscriber;
88
import io.getunleash.event.UnleashSubscriber;
99
import io.getunleash.impactmetrics.ImpactMetricsDataSource;
10+
import io.getunleash.impactmetrics.InMemoryMetricRegistry;
1011
import io.getunleash.lang.Nullable;
1112
import io.getunleash.metric.DefaultHttpMetricsSender;
1213
import io.getunleash.repository.HttpFeatureFetcher;
@@ -74,7 +75,7 @@ public class UnleashConfig {
7475
@Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
7576
@Nullable private final Proxy proxy;
7677
@Nullable private final Consumer<UnleashException> startupExceptionHandler;
77-
@Nullable private final ImpactMetricsDataSource impactMetricsRegistry;
78+
private final ImpactMetricsDataSource impactMetricsRegistry;
7879

7980
private UnleashConfig(
8081
@Nullable URI unleashAPI,
@@ -109,7 +110,7 @@ private UnleashConfig(
109110
@Nullable Proxy proxy,
110111
@Nullable Authenticator proxyAuthenticator,
111112
@Nullable Consumer<UnleashException> startupExceptionHandler,
112-
@Nullable ImpactMetricsDataSource impactMetricsRegistry) {
113+
ImpactMetricsDataSource impactMetricsRegistry) {
113114

114115
if (appName == null) {
115116
throw new IllegalStateException("You are required to specify the unleash appName");
@@ -356,7 +357,6 @@ public Proxy getProxy() {
356357
return proxy;
357358
}
358359

359-
@Nullable
360360
public ImpactMetricsDataSource getImpactMetricsRegistry() {
361361
return impactMetricsRegistry;
362362
}
@@ -766,7 +766,8 @@ public UnleashConfig build() {
766766
proxy,
767767
proxyAuthenticator,
768768
startupExceptionHandler,
769-
impactMetricsRegistry);
769+
Optional.ofNullable(impactMetricsRegistry)
770+
.orElseGet(InMemoryMetricRegistry::new));
770771
}
771772

772773
public String getDefaultSdkVersion() {

src/test/java/io/getunleash/impactmetrics/MetricTypeTest.java

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5-
import com.google.gson.Gson;
6-
import com.google.gson.GsonBuilder;
7-
import io.getunleash.util.HistogramBucketSerializer;
85
import java.util.Collections;
96
import java.util.List;
107
import java.util.Map;
@@ -303,60 +300,4 @@ private NumericMetricSample sample(long value) {
303300
private NumericMetricSample sample(Map<String, String> labels, long value) {
304301
return new NumericMetricSample(labels, value);
305302
}
306-
307-
@Test
308-
public void should_serialize_histogram_with_positive_infinity_as_plus_inf_string() {
309-
InMemoryMetricRegistry registry = new InMemoryMetricRegistry();
310-
Histogram histogram =
311-
registry.histogram(
312-
new BucketMetricOptions(
313-
"serialization_test", "test serialization", List.of(1.0, 5.0)));
314-
315-
histogram.observe(10.0, Map.of("env", "prod"));
316-
317-
List<CollectedMetric> metrics = registry.collect();
318-
assertThat(metrics).hasSize(1);
319-
320-
Gson gson =
321-
new GsonBuilder()
322-
.registerTypeAdapter(HistogramBucket.class, new HistogramBucketSerializer())
323-
.create();
324-
325-
String json = gson.toJson(metrics.get(0));
326-
327-
assertThat(json).contains("\"le\":\"+Inf\"");
328-
assertThat(json).contains("\"le\":1.0");
329-
assertThat(json).contains("\"le\":5.0");
330-
}
331-
332-
@Test
333-
public void should_serialize_histogram_buckets_correctly() {
334-
InMemoryMetricRegistry registry = new InMemoryMetricRegistry();
335-
Histogram histogram =
336-
registry.histogram(
337-
new BucketMetricOptions(
338-
"bucket_serialization", "test buckets", List.of(0.5, 2.0)));
339-
340-
histogram.observe(0.3);
341-
histogram.observe(1.5);
342-
histogram.observe(10.0);
343-
344-
List<CollectedMetric> metrics = registry.collect();
345-
assertThat(metrics).hasSize(1);
346-
347-
BucketMetricSample sample = (BucketMetricSample) metrics.get(0).getSamples().get(0);
348-
assertThat(sample.getBuckets()).hasSize(3);
349-
350-
Gson gson =
351-
new GsonBuilder()
352-
.registerTypeAdapter(HistogramBucket.class, new HistogramBucketSerializer())
353-
.create();
354-
355-
String json = gson.toJson(sample);
356-
357-
assertThat(json).contains("\"le\":0.5");
358-
assertThat(json).contains("\"le\":2.0");
359-
assertThat(json).contains("\"le\":\"+Inf\"");
360-
assertThat(json).contains("\"count\":");
361-
}
362303
}

src/test/java/io/getunleash/metric/DefaultHttpMetricsSenderTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@
1414

1515
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
1616
import io.getunleash.engine.MetricsBucket;
17+
import io.getunleash.impactmetrics.BucketMetricOptions;
18+
import io.getunleash.impactmetrics.CollectedMetric;
19+
import io.getunleash.impactmetrics.Histogram;
20+
import io.getunleash.impactmetrics.InMemoryMetricRegistry;
1721
import io.getunleash.util.UnleashConfig;
1822
import java.net.URI;
1923
import java.net.URISyntaxException;
2024
import java.time.Instant;
2125
import java.time.LocalDateTime;
2226
import java.util.HashSet;
27+
import java.util.List;
2328
import org.junit.jupiter.api.Test;
2429
import org.junit.jupiter.api.extension.RegisterExtension;
2530

@@ -108,4 +113,44 @@ public void should_handle_service_failure_when_sending_metrics() throws URISynta
108113
.withHeader(UNLEASH_INTERVAL, matching(String.valueOf(metricsInterval)))
109114
.withHeader("UNLEASH-APPNAME", matching("test-app")));
110115
}
116+
117+
@Test
118+
public void should_send_impact_metrics_with_histogram_and_plus_inf_bucket()
119+
throws URISyntaxException {
120+
stubFor(
121+
post(urlEqualTo("/client/metrics"))
122+
.withHeader("UNLEASH-APPNAME", matching("test-app"))
123+
.willReturn(aResponse().withStatus(200)));
124+
125+
URI uri = new URI("http://localhost:" + serverMock.getPort());
126+
UnleashConfig config = UnleashConfig.builder().appName("test-app").unleashAPI(uri).build();
127+
128+
InMemoryMetricRegistry registry = new InMemoryMetricRegistry();
129+
Histogram histogram =
130+
registry.histogram(
131+
new BucketMetricOptions(
132+
"test_histogram", "testing histogram", List.of(1.0, 5.0)));
133+
134+
histogram.observe(0.5);
135+
histogram.observe(10.0);
136+
137+
List<CollectedMetric> impactMetrics = registry.collect();
138+
139+
DefaultHttpMetricsSender sender = new DefaultHttpMetricsSender(config);
140+
MetricsBucket bucket = new MetricsBucket(Instant.now(), Instant.now(), null);
141+
ClientMetrics metrics = new ClientMetrics(config, bucket, impactMetrics);
142+
sender.sendMetrics(metrics);
143+
144+
verify(
145+
postRequestedFor(urlMatching("/client/metrics"))
146+
.withRequestBody(matching(".*\"impactMetrics\".*"))
147+
.withRequestBody(matching(".*\"name\"\\s*:\\s*\"test_histogram\".*"))
148+
.withRequestBody(matching(".*\"type\"\\s*:\\s*\"HISTOGRAM\".*"))
149+
.withRequestBody(matching(".*\"le\"\\s*:\\s*\"\\+Inf\".*"))
150+
.withHeader(
151+
UNLEASH_INTERVAL, matching(config.getSendMetricsIntervalMillis()))
152+
.withHeader(
153+
UNLEASH_CONNECTION_ID_HEADER, matching(config.getConnectionId()))
154+
.withHeader("UNLEASH-APPNAME", matching("test-app")));
155+
}
111156
}

src/test/java/io/getunleash/metric/UnleashMetricServiceImplTest.java

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import io.getunleash.engine.UnleashEngine;
99
import io.getunleash.engine.YggdrasilError;
1010
import io.getunleash.engine.YggdrasilInvalidInputException;
11+
import io.getunleash.impactmetrics.CollectedMetric;
12+
import io.getunleash.impactmetrics.ImpactMetricsDataSource;
1113
import io.getunleash.util.UnleashConfig;
1214
import io.getunleash.util.UnleashScheduledExecutor;
1315
import java.time.LocalDateTime;
16+
import java.util.Collections;
1417
import java.util.HashSet;
18+
import java.util.List;
1519
import java.util.Set;
1620
import org.junit.jupiter.api.Test;
1721
import org.mockito.ArgumentCaptor;
@@ -359,6 +363,129 @@ public void url_not_found_immediately_increases_interval_to_max() throws Yggdras
359363
assertThat(unleashMetricService.getSkips()).isEqualTo(0);
360364
}
361365

366+
@Test
367+
public void should_include_impact_metrics_in_clientmetrics_payload() throws YggdrasilError {
368+
UnleashConfig config =
369+
UnleashConfig.builder()
370+
.appName("test")
371+
.sendMetricsInterval(10)
372+
.unleashAPI("http://unleash.com")
373+
.impactMetricsRegistry(mock(ImpactMetricsDataSource.class))
374+
.build();
375+
376+
UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
377+
DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
378+
UnleashEngine engine = new UnleashEngine();
379+
ImpactMetricsDataSource registry = mock(ImpactMetricsDataSource.class);
380+
381+
CollectedMetric sample =
382+
new CollectedMetric(
383+
"feature_toggle_used",
384+
"tracks toggle usage",
385+
io.getunleash.impactmetrics.MetricType.COUNTER,
386+
Collections.emptyList());
387+
when(registry.collect()).thenReturn(List.of(sample));
388+
389+
UnleashConfig configWithRegistry =
390+
UnleashConfig.builder()
391+
.appName("test")
392+
.sendMetricsInterval(10)
393+
.unleashAPI("http://unleash.com")
394+
.impactMetricsRegistry(registry)
395+
.build();
396+
397+
UnleashMetricServiceImpl unleashMetricService =
398+
new UnleashMetricServiceImpl(configWithRegistry, sender, executor, engine);
399+
400+
ArgumentCaptor<Runnable> sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class);
401+
verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong());
402+
403+
when(sender.sendMetrics(any(ClientMetrics.class))).thenReturn(200);
404+
405+
sendMetricsCallback.getValue().run();
406+
407+
ArgumentCaptor<ClientMetrics> cmCaptor = ArgumentCaptor.forClass(ClientMetrics.class);
408+
verify(sender).sendMetrics(cmCaptor.capture());
409+
410+
ClientMetrics metricsSent = cmCaptor.getValue();
411+
assertThat(metricsSent.getImpactMetrics()).isNotNull();
412+
assertThat(metricsSent.getImpactMetrics()).hasSize(1);
413+
assertThat(metricsSent.getImpactMetrics().get(0).getName())
414+
.isEqualTo("feature_toggle_used");
415+
}
416+
417+
@Test
418+
public void should_restore_impact_metrics_on_failure() throws YggdrasilError {
419+
ImpactMetricsDataSource registry = mock(ImpactMetricsDataSource.class);
420+
421+
CollectedMetric sample =
422+
new CollectedMetric(
423+
"feature_toggle_used",
424+
"tracks toggle usage",
425+
io.getunleash.impactmetrics.MetricType.COUNTER,
426+
Collections.emptyList());
427+
when(registry.collect()).thenReturn(List.of(sample));
428+
429+
UnleashConfig config =
430+
UnleashConfig.builder()
431+
.appName("test")
432+
.sendMetricsInterval(10)
433+
.unleashAPI("http://unleash.com")
434+
.impactMetricsRegistry(registry)
435+
.build();
436+
437+
UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
438+
DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
439+
UnleashEngine engine = new UnleashEngine();
440+
441+
UnleashMetricServiceImpl unleashMetricService =
442+
new UnleashMetricServiceImpl(config, sender, executor, engine);
443+
444+
ArgumentCaptor<Runnable> sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class);
445+
verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong());
446+
447+
when(sender.sendMetrics(any(ClientMetrics.class))).thenReturn(500);
448+
449+
sendMetricsCallback.getValue().run();
450+
451+
verify(registry, times(1)).restore(List.of(sample));
452+
}
453+
454+
@Test
455+
public void should_not_include_impact_metrics_field_when_empty() throws YggdrasilError {
456+
ImpactMetricsDataSource registry = mock(ImpactMetricsDataSource.class);
457+
when(registry.collect()).thenReturn(Collections.emptyList());
458+
459+
UnleashConfig config =
460+
UnleashConfig.builder()
461+
.appName("test")
462+
.sendMetricsInterval(10)
463+
.unleashAPI("http://unleash.com")
464+
.impactMetricsRegistry(registry)
465+
.build();
466+
467+
UnleashScheduledExecutor executor = mock(UnleashScheduledExecutor.class);
468+
DefaultHttpMetricsSender sender = mock(DefaultHttpMetricsSender.class);
469+
UnleashEngine engine = new UnleashEngine();
470+
471+
UnleashMetricServiceImpl unleashMetricService =
472+
new UnleashMetricServiceImpl(config, sender, executor, engine);
473+
474+
ArgumentCaptor<Runnable> sendMetricsCallback = ArgumentCaptor.forClass(Runnable.class);
475+
verify(executor).setInterval(sendMetricsCallback.capture(), anyLong(), anyLong());
476+
477+
when(sender.sendMetrics(any(ClientMetrics.class))).thenReturn(200);
478+
479+
sendMetricsCallback.getValue().run();
480+
481+
ArgumentCaptor<ClientMetrics> cmCaptor = ArgumentCaptor.forClass(ClientMetrics.class);
482+
verify(sender).sendMetrics(cmCaptor.capture());
483+
484+
ClientMetrics metricsSent = cmCaptor.getValue();
485+
assertThat(metricsSent.getImpactMetrics()).isNull();
486+
verify(registry, times(1)).collect();
487+
}
488+
362489
@Test
363490
public void should_add_new_metrics_data_to_bucket() {
364491
UnleashConfig config =

0 commit comments

Comments
 (0)