Skip to content

Commit 3cc9310

Browse files
feat!: otel metric hook (#332)
Signed-off-by: Kavindu Dodanduwa <[email protected]>
1 parent 220b01a commit 3cc9310

File tree

8 files changed

+475
-68
lines changed

8 files changed

+475
-68
lines changed

hooks/open-telemetry/pom.xml

+51-40
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,59 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
3-
<modelVersion>4.0.0</modelVersion>
4-
<parent>
5-
<groupId>dev.openfeature.contrib</groupId>
6-
<artifactId>parent</artifactId>
7-
<version>0.1.0</version>
8-
<relativePath>../../pom.xml</relativePath>
9-
</parent>
10-
<groupId>dev.openfeature.contrib.hooks</groupId>
11-
<artifactId>otel</artifactId>
12-
<version>1.0.3</version> <!--x-release-please-version -->
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>0.1.0</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
<groupId>dev.openfeature.contrib.hooks</groupId>
12+
<artifactId>otel</artifactId>
13+
<version>2.0.0</version> <!--x-release-please-version -->
1314

14-
<name>open-telemetry-hook</name>
15-
<description>Open Telemetry Hook</description>
16-
<url>https://openfeature.dev</url>
15+
<name>open-telemetry-hook</name>
16+
<description>Open Telemetry Hook</description>
17+
<url>https://openfeature.dev</url>
1718

18-
<developers>
19-
<developer>
20-
<id>toddbaert</id>
21-
<name>Todd Baert</name>
22-
<organization>OpenFeature</organization>
23-
<url>https://openfeature.dev/</url>
24-
</developer>
25-
</developers>
19+
<developers>
20+
<developer>
21+
<id>toddbaert</id>
22+
<name>Todd Baert</name>
23+
<organization>OpenFeature</organization>
24+
<url>https://openfeature.dev/</url>
25+
</developer>
26+
</developers>
2627

27-
<dependencies>
28-
<!-- we inherent dev.openfeature.javasdk and the test dependencies from the parent pom -->
29-
<dependency>
30-
<groupId>io.opentelemetry</groupId>
31-
<artifactId>opentelemetry-sdk</artifactId>
32-
</dependency>
28+
<dependencyManagement>
29+
<dependencies>
30+
<dependency>
31+
<groupId>io.opentelemetry</groupId>
32+
<artifactId>opentelemetry-bom</artifactId>
33+
<version>1.28.0</version>
34+
<type>pom</type>
35+
<scope>import</scope>
36+
</dependency>
37+
</dependencies>
38+
</dependencyManagement>
3339

34-
</dependencies>
40+
<dependencies>
41+
<dependency>
42+
<groupId>dev.openfeature</groupId>
43+
<artifactId>sdk</artifactId>
44+
<version>[1.4,2.0)</version>
45+
<scope>provided</scope>
46+
</dependency>
3547

36-
<dependencyManagement>
37-
<dependencies>
38-
<dependency>
39-
<groupId>io.opentelemetry</groupId>
40-
<artifactId>opentelemetry-bom</artifactId>
41-
<version>1.28.0</version>
42-
<type>pom</type>
43-
<scope>import</scope>
44-
</dependency>
45-
</dependencies>
46-
</dependencyManagement>
48+
<dependency>
49+
<groupId>io.opentelemetry</groupId>
50+
<artifactId>opentelemetry-api</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>io.opentelemetry</groupId>
54+
<artifactId>opentelemetry-sdk-testing</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
</dependencies>
4758

4859
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dev.openfeature.contrib.hooks.otel;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
/**
7+
* Represents an OTel dimension(attribute) Key and Type of the dimension.
8+
*/
9+
@Getter
10+
@AllArgsConstructor
11+
public class DimensionDescription {
12+
private final String key;
13+
private final Class type;
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package dev.openfeature.contrib.hooks.otel;
2+
3+
import dev.openfeature.sdk.EvaluationContext;
4+
import dev.openfeature.sdk.FlagEvaluationDetails;
5+
import dev.openfeature.sdk.Hook;
6+
import dev.openfeature.sdk.HookContext;
7+
import dev.openfeature.sdk.ImmutableMetadata;
8+
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.common.AttributesBuilder;
11+
import io.opentelemetry.api.metrics.LongCounter;
12+
import io.opentelemetry.api.metrics.LongUpDownCounter;
13+
import io.opentelemetry.api.metrics.Meter;
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
import java.util.Collections;
17+
import java.util.List;
18+
import java.util.Map;
19+
import java.util.Optional;
20+
21+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.ERROR_KEY;
22+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.REASON_KEY;
23+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.flagKeyAttributeKey;
24+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.providerNameAttributeKey;
25+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.variantAttributeKey;
26+
27+
/**
28+
* OpenTelemetry metric hook records metrics at different {@link Hook} stages.
29+
*/
30+
@Slf4j
31+
@SuppressWarnings("PMD.TooManyStaticImports")
32+
public class MetricsHook implements Hook {
33+
34+
private static final String METER_NAME = "java.openfeature.dev";
35+
private static final String EVALUATION_ACTIVE_COUNT = "feature_flag.evaluation_active_count";
36+
private static final String EVALUATION_REQUESTS_TOTAL = "feature_flag.evaluation_requests_total";
37+
private static final String FLAG_EVALUATION_SUCCESS_TOTAL = "feature_flag.evaluation_success_total";
38+
private static final String FLAG_EVALUATION_ERROR_TOTAL = "feature_flag.evaluation_error_total";
39+
40+
private final LongUpDownCounter activeFlagEvaluationsCounter;
41+
private final LongCounter evaluationRequestCounter;
42+
private final LongCounter evaluationSuccessCounter;
43+
private final LongCounter evaluationErrorCounter;
44+
private final List<DimensionDescription> dimensionDescriptions;
45+
46+
/**
47+
* Construct a metric hook by providing an {@link OpenTelemetry} instance.
48+
*/
49+
public MetricsHook(final OpenTelemetry openTelemetry) {
50+
this(openTelemetry, Collections.emptyList());
51+
}
52+
53+
/**
54+
* Construct a metric hook with {@link OpenTelemetry} instance and a list of {@link DimensionDescription}.
55+
* Provided dimensions are attempted to be extracted from ImmutableMetadata attached to
56+
* {@link FlagEvaluationDetails}.
57+
*/
58+
public MetricsHook(final OpenTelemetry openTelemetry, final List<DimensionDescription> dimensions) {
59+
final Meter meter = openTelemetry.getMeter(METER_NAME);
60+
61+
activeFlagEvaluationsCounter =
62+
meter.upDownCounterBuilder(EVALUATION_ACTIVE_COUNT).setDescription("active flag evaluations counter")
63+
.build();
64+
65+
evaluationRequestCounter = meter.counterBuilder(EVALUATION_REQUESTS_TOTAL)
66+
.setDescription("feature flag evaluation request counter").build();
67+
68+
evaluationSuccessCounter = meter.counterBuilder(FLAG_EVALUATION_SUCCESS_TOTAL)
69+
.setDescription("feature flag evaluation success counter").build();
70+
71+
evaluationErrorCounter = meter.counterBuilder(FLAG_EVALUATION_ERROR_TOTAL)
72+
.setDescription("feature flag evaluation error counter").build();
73+
74+
dimensionDescriptions = Collections.unmodifiableList(dimensions);
75+
}
76+
77+
78+
@Override
79+
public Optional<EvaluationContext> before(HookContext ctx, Map hints) {
80+
activeFlagEvaluationsCounter.add(+1, Attributes.of(flagKeyAttributeKey, ctx.getFlagKey()));
81+
82+
evaluationRequestCounter.add(+1, Attributes.of(flagKeyAttributeKey, ctx.getFlagKey(), providerNameAttributeKey,
83+
ctx.getProviderMetadata().getName()));
84+
return Optional.empty();
85+
}
86+
87+
@Override
88+
public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
89+
final AttributesBuilder attributesBuilder = Attributes.builder();
90+
91+
attributesBuilder.put(flagKeyAttributeKey, ctx.getFlagKey());
92+
attributesBuilder.put(providerNameAttributeKey, ctx.getProviderMetadata().getName());
93+
94+
if (details.getReason() != null) {
95+
attributesBuilder.put(REASON_KEY, details.getReason());
96+
}
97+
98+
if (details.getVariant() != null) {
99+
attributesBuilder.put(variantAttributeKey, details.getVariant());
100+
} else {
101+
attributesBuilder.put(variantAttributeKey, String.valueOf(details.getValue()));
102+
}
103+
104+
if (!dimensionDescriptions.isEmpty()) {
105+
attributesBuilder.putAll(attributesFromFlagMetadata(details.getFlagMetadata(), dimensionDescriptions));
106+
}
107+
108+
evaluationSuccessCounter.add(+1, attributesBuilder.build());
109+
}
110+
111+
@Override
112+
public void error(HookContext ctx, Exception error, Map hints) {
113+
final AttributesBuilder attributesBuilder = Attributes.builder();
114+
115+
attributesBuilder.put(flagKeyAttributeKey, ctx.getFlagKey());
116+
attributesBuilder.put(providerNameAttributeKey, ctx.getProviderMetadata().getName());
117+
118+
if (error.getMessage() != null) {
119+
attributesBuilder.put(ERROR_KEY, error.getMessage());
120+
}
121+
122+
evaluationErrorCounter.add(+1, attributesBuilder.build());
123+
}
124+
125+
@Override
126+
public void finallyAfter(HookContext ctx, Map hints) {
127+
activeFlagEvaluationsCounter.add(-1, Attributes.of(flagKeyAttributeKey, ctx.getFlagKey()));
128+
}
129+
130+
/**
131+
* A helper to derive attributes from {@link DimensionDescription}.
132+
*/
133+
private static Attributes attributesFromFlagMetadata(final ImmutableMetadata flagMetadata,
134+
final List<DimensionDescription> dimensionDescriptions) {
135+
final AttributesBuilder builder = Attributes.builder();
136+
137+
for (DimensionDescription dimension : dimensionDescriptions) {
138+
final Object value = flagMetadata.getValue(dimension.getKey(), dimension.getType());
139+
140+
if (value == null) {
141+
log.debug("No value mapping found for key " + dimension.getKey() + " of type "
142+
+ dimension.getType().getSimpleName());
143+
continue;
144+
}
145+
146+
if (dimension.getType().equals(String.class)) {
147+
builder.put(dimension.getKey(), (String) value);
148+
} else if (dimension.getType().equals(Integer.class)) {
149+
builder.put(dimension.getKey(), (Integer) value);
150+
}
151+
if (dimension.getType().equals(Long.class)) {
152+
builder.put(dimension.getKey(), (Long) value);
153+
}
154+
if (dimension.getType().equals(Float.class)) {
155+
builder.put(dimension.getKey(), (Float) value);
156+
}
157+
if (dimension.getType().equals(Double.class)) {
158+
builder.put(dimension.getKey(), (Double) value);
159+
}
160+
if (dimension.getType().equals(Boolean.class)) {
161+
builder.put(dimension.getKey(), (Boolean) value);
162+
}
163+
}
164+
165+
return builder.build();
166+
}
167+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.openfeature.contrib.hooks.otel;
2+
3+
import io.opentelemetry.api.common.AttributeKey;
4+
5+
class OTelCommons {
6+
// Define semantic conventions
7+
// Refer - https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/
8+
static final String EVENT_NAME = "feature_flag";
9+
static final AttributeKey<String> flagKeyAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".flag_key");
10+
static final AttributeKey<String> providerNameAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".provider_name");
11+
static final AttributeKey<String> variantAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".variant");
12+
13+
// Define non convention attribute keys
14+
static final String REASON_KEY = "reason";
15+
static final String ERROR_KEY = "exception";
16+
}

hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/OpenTelemetryHook.java renamed to hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/TracesHook.java

+9-12
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,35 @@
33
import dev.openfeature.sdk.FlagEvaluationDetails;
44
import dev.openfeature.sdk.Hook;
55
import dev.openfeature.sdk.HookContext;
6-
import io.opentelemetry.api.common.AttributeKey;
76
import io.opentelemetry.api.common.Attributes;
87
import io.opentelemetry.api.trace.Span;
98
import io.opentelemetry.api.trace.StatusCode;
109

1110
import java.util.Map;
1211

12+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.EVENT_NAME;
13+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.flagKeyAttributeKey;
14+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.providerNameAttributeKey;
15+
import static dev.openfeature.contrib.hooks.otel.OTelCommons.variantAttributeKey;
16+
1317
/**
1418
* The OpenTelemetry hook provides a way to automatically add a feature flag evaluation to a span as a span event.
1519
* Refer to <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/feature-flags.md">OpenTelemetry</a>
1620
*/
17-
public class OpenTelemetryHook implements Hook {
18-
19-
private static final String EVENT_NAME = "feature_flag";
20-
21+
public class TracesHook implements Hook {
2122
private final boolean setSpanErrorStatus;
2223

23-
private final AttributeKey<String> flagKeyAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".flag_key");
24-
private final AttributeKey<String> providerNameAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".provider_name");
25-
private final AttributeKey<String> variantAttributeKey = AttributeKey.stringKey(EVENT_NAME + ".variant");
26-
2724
/**
2825
* Create a new OpenTelemetryHook instance with default options.
2926
*/
30-
public OpenTelemetryHook() {
31-
this(OpenTelemetryHookOptions.builder().build());
27+
public TracesHook() {
28+
this(TracesHookOptions.builder().build());
3229
}
3330

3431
/**
3532
* Create a new OpenTelemetryHook instance with options.
3633
*/
37-
public OpenTelemetryHook(OpenTelemetryHookOptions options) {
34+
public TracesHook(TracesHookOptions options) {
3835
setSpanErrorStatus = options.isSetSpanErrorStatus();
3936
}
4037

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
*/
99
@Builder
1010
@Getter
11-
public class OpenTelemetryHookOptions {
12-
11+
public class TracesHookOptions {
1312
/**
1413
* Control Span error status. Default is false - Span status is unchanged for hook error
1514
*/

0 commit comments

Comments
 (0)