Skip to content

Commit 04c42e3

Browse files
authored
feat: add user facing impact metrics (#342)
1 parent 8c0f334 commit 04c42e3

14 files changed

Lines changed: 538 additions & 20 deletions

src/main/java/io/getunleash/DefaultUnleash.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import io.getunleash.event.IsEnabledImpressionEvent;
99
import io.getunleash.event.ToggleEvaluated;
1010
import io.getunleash.event.VariantImpressionEvent;
11+
import io.getunleash.impactmetrics.ImpactMetricRegistryAndDataSource;
12+
import io.getunleash.impactmetrics.MetricsAPI;
13+
import io.getunleash.impactmetrics.StaticContext;
14+
import io.getunleash.impactmetrics.VariantResolver;
1115
import io.getunleash.repository.FeatureRepository;
1216
import io.getunleash.repository.YggdrasilAdapters;
1317
import io.getunleash.strategy.*;
@@ -30,6 +34,7 @@ public class DefaultUnleash implements Unleash {
3034
private final UnleashContextProvider contextProvider;
3135
private final EventDispatcher eventDispatcher;
3236
private final UnleashConfig config;
37+
private final MetricsAPI impactMetrics;
3338

3439
private static EngineProxy defaultToggleRepository(
3540
UnleashConfig unleashConfig, Strategy... strategies) {
@@ -69,6 +74,13 @@ public DefaultUnleash(
6974
this.featureRepository = engineProxy;
7075
this.contextProvider = contextProvider;
7176
this.eventDispatcher = eventDispatcher;
77+
78+
ImpactMetricRegistryAndDataSource registry = unleashConfig.getImpactMetricsRegistry();
79+
VariantResolver variantResolver = this::getVariantForImpactMetrics;
80+
StaticContext staticContext =
81+
new StaticContext(unleashConfig.getAppName(), unleashConfig.getEnvironment());
82+
this.impactMetrics = new MetricsAPI(registry, variantResolver, staticContext);
83+
7284
initCounts.compute(
7385
config.getClientIdentifier(),
7486
(key, inits) -> {
@@ -139,12 +151,8 @@ public Variant getVariant(String toggleName, Variant defaultValue) {
139151

140152
@Override
141153
public Variant getVariant(String toggleName, UnleashContext context, Variant defaultValue) {
142-
UnleashContext enhancedContext = context.applyStaticFields(config);
143-
Optional<FlatResponse<VariantDef>> response =
144-
Optional.ofNullable(this.featureRepository.getVariant(toggleName, enhancedContext));
145-
Optional<VariantDef> variantDef = response.map(r -> r.value);
146-
147-
Variant variant = YggdrasilAdapters.adapt(variantDef, defaultValue);
154+
Optional<FlatResponse<VariantDef>> response = getVariantResponse(toggleName, context);
155+
Variant variant = resolveVariant(response, defaultValue);
148156
eventDispatcher.dispatch(new ToggleEvaluated(toggleName, variant.isFeatureEnabled()));
149157
if (response.map(r -> r.impressionData).orElse(false)) {
150158
eventDispatcher.dispatch(
@@ -154,6 +162,23 @@ public Variant getVariant(String toggleName, UnleashContext context, Variant def
154162
return variant;
155163
}
156164

165+
private Variant getVariantForImpactMetrics(String toggleName, UnleashContext context) {
166+
Optional<FlatResponse<VariantDef>> response = getVariantResponse(toggleName, context);
167+
return resolveVariant(response, Variant.DISABLED_VARIANT);
168+
}
169+
170+
private Optional<FlatResponse<VariantDef>> getVariantResponse(
171+
String toggleName, UnleashContext context) {
172+
UnleashContext enhancedContext = context.applyStaticFields(config);
173+
return Optional.ofNullable(this.featureRepository.getVariant(toggleName, enhancedContext));
174+
}
175+
176+
private Variant resolveVariant(
177+
Optional<FlatResponse<VariantDef>> response, Variant defaultValue) {
178+
Optional<VariantDef> variantDef = response.map(r -> r.value);
179+
return YggdrasilAdapters.adapt(variantDef, defaultValue);
180+
}
181+
157182
@Override
158183
public void shutdown() {
159184
featureRepository.shutdown();
@@ -165,6 +190,11 @@ public MoreOperations more() {
165190
return new DefaultMore();
166191
}
167192

193+
@Override
194+
public MetricsAPI getImpactMetrics() {
195+
return impactMetrics;
196+
}
197+
168198
public class DefaultMore implements MoreOperations {
169199

170200
@Override

src/main/java/io/getunleash/FakeUnleash.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package io.getunleash;
22

3+
import io.getunleash.impactmetrics.ImpactMetricRegistry;
4+
import io.getunleash.impactmetrics.InMemoryMetricRegistry;
5+
import io.getunleash.impactmetrics.MetricsAPI;
6+
import io.getunleash.impactmetrics.StaticContext;
7+
import io.getunleash.impactmetrics.VariantResolver;
38
import io.getunleash.lang.Nullable;
49
import io.getunleash.variant.Variant;
510
import java.util.*;
@@ -24,6 +29,14 @@ public class FakeUnleash implements Unleash {
2429
private final Map<String, Boolean> excludedFeatures = new ConcurrentHashMap<>();
2530
private final Map<String, Boolean> features = new ConcurrentHashMap<>();
2631
private final Map<String, Variant> variants = new ConcurrentHashMap<>();
32+
private final MetricsAPI impactMetrics;
33+
34+
public FakeUnleash() {
35+
ImpactMetricRegistry registry = new InMemoryMetricRegistry();
36+
VariantResolver variantResolver = (flagName, context) -> Variant.DISABLED_VARIANT;
37+
StaticContext staticContext = new StaticContext("fake-app", "test");
38+
this.impactMetrics = new MetricsAPI(registry, variantResolver, staticContext);
39+
}
2740

2841
@Override
2942
public boolean isEnabled(
@@ -83,6 +96,11 @@ public MoreOperations more() {
8396
return new FakeMore();
8497
}
8598

99+
@Override
100+
public MetricsAPI getImpactMetrics() {
101+
return impactMetrics;
102+
}
103+
86104
public void enableAll() {
87105
disableAll = false;
88106
enableAll = true;

src/main/java/io/getunleash/Unleash.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.getunleash;
22

3+
import io.getunleash.impactmetrics.MetricsAPI;
34
import io.getunleash.variant.Variant;
45
import java.util.function.BiPredicate;
56

@@ -46,4 +47,6 @@ default Variant getVariant(final String toggleName, final Variant defaultValue)
4647
default void shutdown() {}
4748

4849
MoreOperations more();
50+
51+
MetricsAPI getImpactMetrics();
4952
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
package io.getunleash.impactmetrics;
22

3+
import io.getunleash.lang.Nullable;
4+
35
public interface ImpactMetricRegistry {
46
Counter counter(MetricOptions options);
57

68
Gauge gauge(MetricOptions options);
79

810
Histogram histogram(BucketMetricOptions options);
11+
12+
@Nullable
13+
Counter getCounter(String name);
14+
15+
@Nullable
16+
Gauge getGauge(String name);
17+
18+
@Nullable
19+
Histogram getHistogram(String name);
920
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package io.getunleash.impactmetrics;
2+
3+
public interface ImpactMetricRegistryAndDataSource
4+
extends ImpactMetricRegistry, ImpactMetricsDataSource {}

src/main/java/io/getunleash/impactmetrics/InMemoryMetricRegistry.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.util.Map;
66
import java.util.concurrent.ConcurrentHashMap;
77

8-
public class InMemoryMetricRegistry implements ImpactMetricRegistry, ImpactMetricsDataSource {
8+
public class InMemoryMetricRegistry implements ImpactMetricRegistryAndDataSource {
99
private final Map<String, CounterImpl> counters = new ConcurrentHashMap<>();
1010
private final Map<String, GaugeImpl> gauges = new ConcurrentHashMap<>();
1111
private final Map<String, HistogramImpl> histograms = new ConcurrentHashMap<>();
@@ -25,6 +25,21 @@ public Histogram histogram(BucketMetricOptions options) {
2525
return histograms.computeIfAbsent(options.getName(), name -> new HistogramImpl(options));
2626
}
2727

28+
@Override
29+
public Counter getCounter(String name) {
30+
return counters.get(name);
31+
}
32+
33+
@Override
34+
public Gauge getGauge(String name) {
35+
return gauges.get(name);
36+
}
37+
38+
@Override
39+
public Histogram getHistogram(String name) {
40+
return histograms.get(name);
41+
}
42+
2843
@Override
2944
public List<CollectedMetric> collect() {
3045
List<CollectedMetric> collected = new ArrayList<>();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.getunleash.impactmetrics;
2+
3+
import io.getunleash.UnleashContext;
4+
import java.util.List;
5+
6+
public class MetricFlagContext {
7+
private final List<String> flagNames;
8+
private final UnleashContext context;
9+
10+
public MetricFlagContext(List<String> flagNames, UnleashContext context) {
11+
this.flagNames = flagNames;
12+
this.context = context;
13+
}
14+
15+
public List<String> getFlagNames() {
16+
return flagNames;
17+
}
18+
19+
public UnleashContext getContext() {
20+
return context;
21+
}
22+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package io.getunleash.impactmetrics;
2+
3+
import io.getunleash.lang.Nullable;
4+
import io.getunleash.variant.Variant;
5+
import java.util.ArrayList;
6+
import java.util.HashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
public class MetricsAPI {
13+
private static final Logger LOGGER = LoggerFactory.getLogger(MetricsAPI.class);
14+
15+
private final ImpactMetricRegistry metricRegistry;
16+
private final VariantResolver variantResolver;
17+
private final StaticContext staticContext;
18+
19+
public MetricsAPI(
20+
ImpactMetricRegistry metricRegistry,
21+
VariantResolver variantResolver,
22+
StaticContext staticContext) {
23+
this.metricRegistry = metricRegistry;
24+
this.variantResolver = variantResolver;
25+
this.staticContext = staticContext;
26+
}
27+
28+
public void defineCounter(String name, String help) {
29+
if (name == null || name.isEmpty() || help == null || help.isEmpty()) {
30+
LOGGER.warn("Counter name or help cannot be empty: {}, {}", name, help);
31+
return;
32+
}
33+
List<String> labelNames = List.of("featureName", "appName", "environment");
34+
metricRegistry.counter(new MetricOptions(name, help, labelNames));
35+
}
36+
37+
public void defineGauge(String name, String help) {
38+
if (name == null || name.isEmpty() || help == null || help.isEmpty()) {
39+
LOGGER.warn("Gauge name or help cannot be empty: {}, {}", name, help);
40+
return;
41+
}
42+
List<String> labelNames = List.of("featureName", "appName", "environment");
43+
metricRegistry.gauge(new MetricOptions(name, help, labelNames));
44+
}
45+
46+
public void defineHistogram(String name, String help, @Nullable List<Double> buckets) {
47+
if (name == null || name.isEmpty() || help == null || help.isEmpty()) {
48+
LOGGER.warn("Histogram name or help cannot be empty: {}, {}", name, help);
49+
return;
50+
}
51+
List<String> labelNames = List.of("featureName", "appName", "environment");
52+
List<Double> bucketList = buckets != null ? buckets : new ArrayList<>();
53+
metricRegistry.histogram(new BucketMetricOptions(name, help, labelNames, bucketList));
54+
}
55+
56+
private Map<String, String> getFlagLabels(@Nullable MetricFlagContext flagContext) {
57+
Map<String, String> flagLabels = new HashMap<>();
58+
if (flagContext != null) {
59+
for (String flag : flagContext.getFlagNames()) {
60+
Variant variant =
61+
variantResolver.getVariantForImpactMetrics(flag, flagContext.getContext());
62+
63+
if (variant.isEnabled()) {
64+
flagLabels.put(flag, variant.getName());
65+
} else if (variant.isFeatureEnabled()) {
66+
flagLabels.put(flag, "enabled");
67+
} else {
68+
flagLabels.put(flag, "disabled");
69+
}
70+
}
71+
}
72+
return flagLabels;
73+
}
74+
75+
public void incrementCounter(String name) {
76+
incrementCounter(name, null, null);
77+
}
78+
79+
public void incrementCounter(String name, long value) {
80+
incrementCounter(name, value, null);
81+
}
82+
83+
public void incrementCounter(
84+
String name, @Nullable Long value, @Nullable MetricFlagContext flagContext) {
85+
Counter counter = metricRegistry.getCounter(name);
86+
if (counter == null) {
87+
LOGGER.warn("Counter {} not defined, this counter will not be incremented.", name);
88+
return;
89+
}
90+
91+
Map<String, String> flagLabels = getFlagLabels(flagContext);
92+
Map<String, String> labels = new HashMap<>(flagLabels);
93+
labels.put("appName", staticContext.getAppName());
94+
labels.put("environment", staticContext.getEnvironment());
95+
96+
counter.inc(value != null ? value : 1L, labels);
97+
}
98+
99+
public void updateGauge(String name, long value) {
100+
updateGauge(name, value, null);
101+
}
102+
103+
public void updateGauge(String name, long value, @Nullable MetricFlagContext flagContext) {
104+
Gauge gauge = metricRegistry.getGauge(name);
105+
if (gauge == null) {
106+
LOGGER.warn("Gauge {} not defined, this gauge will not be updated.", name);
107+
return;
108+
}
109+
110+
Map<String, String> flagLabels = getFlagLabels(flagContext);
111+
Map<String, String> labels = new HashMap<>(flagLabels);
112+
labels.put("appName", staticContext.getAppName());
113+
labels.put("environment", staticContext.getEnvironment());
114+
115+
gauge.set(value, labels);
116+
}
117+
118+
public void observeHistogram(String name, double value) {
119+
observeHistogram(name, value, null);
120+
}
121+
122+
public void observeHistogram(
123+
String name, double value, @Nullable MetricFlagContext flagContext) {
124+
Histogram histogram = metricRegistry.getHistogram(name);
125+
if (histogram == null) {
126+
LOGGER.warn("Histogram {} not defined, this histogram will not be updated.", name);
127+
return;
128+
}
129+
130+
Map<String, String> flagLabels = getFlagLabels(flagContext);
131+
Map<String, String> labels = new HashMap<>(flagLabels);
132+
labels.put("appName", staticContext.getAppName());
133+
labels.put("environment", staticContext.getEnvironment());
134+
135+
histogram.observe(value, labels);
136+
}
137+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.getunleash.impactmetrics;
2+
3+
public class StaticContext {
4+
private final String appName;
5+
private final String environment;
6+
7+
public StaticContext(String appName, String environment) {
8+
this.appName = appName;
9+
this.environment = environment;
10+
}
11+
12+
public String getAppName() {
13+
return appName;
14+
}
15+
16+
public String getEnvironment() {
17+
return environment;
18+
}
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.getunleash.impactmetrics;
2+
3+
import io.getunleash.UnleashContext;
4+
import io.getunleash.variant.Variant;
5+
6+
public interface VariantResolver {
7+
Variant getVariantForImpactMetrics(String flagName, UnleashContext context);
8+
}

0 commit comments

Comments
 (0)