Skip to content

Commit ed75f09

Browse files
feat: hook builder options for dimension extraction (#391)
Signed-off-by: Kavindu Dodanduwa <[email protected]> Signed-off-by: Kavindu Dodanduwa <[email protected]>
1 parent 66482a0 commit ed75f09

File tree

7 files changed

+214
-6
lines changed

7 files changed

+214
-6
lines changed

hooks/open-telemetry/README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,22 @@ The hook implementation is attached to `after`and `error` [hook stages](https://
3333
Both successful and failed flag evaluations will add a span event named `feature_flag` with evaluation details such as flag key, provider name and variant.
3434
Failed evaluations can be allowed to set span status to `ERROR`. You can configure this behavior through`TracesHookOptions`.
3535

36-
Consider the following code example for usage,
36+
Further, you can write your own logic to extract custom dimensions from [flag evaluation metadata](https://github.com/open-feature/spec/blob/main/specification/types.md#flag-metadata) by setting a callback to `TracesHookOptions.dimensionExtractor`.
37+
Extracted dimensions will be added to successful falg evaluation spans.
38+
39+
```java
40+
TracesHookOptions options = TracesHookOptions.builder()
41+
// configure a callback
42+
.dimensionExtractor(metadata -> Attributes.builder()
43+
.put("boolean", metadata.getBoolean("boolean"))
44+
.put("integer", metadata.getInteger("integer"))
45+
.build()
46+
).build();
47+
48+
TracesHook tracesHook = new TracesHook(options);
49+
```
50+
51+
Consider the following code example for a complete usage,
3752

3853
```java
3954
final Tracer tracer=... // derive Tracer from OpenTelemetry
@@ -95,3 +110,19 @@ OpenFeatureAPI api = OpenFeatureAPI.getInstance();
95110
// Register MetricsHook with custom dimensions
96111
api.addHooks(new MetricsHook(openTelemetry, customDimensions));
97112
```
113+
114+
Alternatively, you can wrtie your own extraction logic against [flag evaluation metadata](https://github.com/open-feature/spec/blob/main/specification/types.md#flag-metadata) by providing a callback to `TracesHookOptions.attributeSetter`.
115+
116+
```java
117+
final OpenTelemetry openTelemetry = ... // OpenTelemetry API instance
118+
119+
MetricHookOptions hookOptions = MetricHookOptions.builder()
120+
// configure a callback
121+
.attributeSetter(metadata -> Attributes.builder()
122+
.put("boolean", metadata.getBoolean("boolean"))
123+
.put("integer", metadata.getInteger("integer"))
124+
.build())
125+
.build();
126+
127+
final MetricsHook metricHook = new MetricsHook(openTelemetry, hookOptions);
128+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package dev.openfeature.contrib.hooks.otel;
2+
3+
import dev.openfeature.sdk.ImmutableMetadata;
4+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5+
import io.opentelemetry.api.common.Attributes;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.function.Function;
12+
13+
/**
14+
* OpenTelemetry hook options.
15+
*/
16+
@Builder
17+
@Getter
18+
@SuppressFBWarnings(value = {"EI_EXPOSE_REP"}, justification = "Dimension list can be mutable")
19+
20+
public class MetricHookOptions {
21+
22+
/**
23+
* Custom handler to derive {@link Attributes} from flag evaluation metadata represented with
24+
* {@link ImmutableMetadata}.
25+
*/
26+
private Function<ImmutableMetadata, Attributes> attributeSetter;
27+
28+
/**
29+
* List of {@link DimensionDescription} to be extracted from flag evaluation metadata.
30+
*/
31+
@Builder.Default
32+
private final List<DimensionDescription> setDimensions = Collections.emptyList();
33+
}

hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/MetricsHook.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.List;
1818
import java.util.Map;
1919
import java.util.Optional;
20+
import java.util.function.Function;
2021

2122
import static dev.openfeature.contrib.hooks.otel.OTelCommons.ERROR_KEY;
2223
import static dev.openfeature.contrib.hooks.otel.OTelCommons.REASON_KEY;
@@ -42,6 +43,7 @@ public class MetricsHook implements Hook {
4243
private final LongCounter evaluationSuccessCounter;
4344
private final LongCounter evaluationErrorCounter;
4445
private final List<DimensionDescription> dimensionDescriptions;
46+
private final Function<ImmutableMetadata, Attributes> extractor;
4547

4648
/**
4749
* Construct a metric hook by providing an {@link OpenTelemetry} instance.
@@ -54,8 +56,18 @@ public MetricsHook(final OpenTelemetry openTelemetry) {
5456
* Construct a metric hook with {@link OpenTelemetry} instance and a list of {@link DimensionDescription}.
5557
* Provided dimensions are attempted to be extracted from ImmutableMetadata attached to
5658
* {@link FlagEvaluationDetails}.
59+
*
60+
* @deprecated - This constructor is deprecated. Please use {@link MetricHookOptions} based options
5761
*/
62+
@Deprecated
5863
public MetricsHook(final OpenTelemetry openTelemetry, final List<DimensionDescription> dimensions) {
64+
this(openTelemetry, MetricHookOptions.builder().setDimensions(dimensions).build());
65+
}
66+
67+
/**
68+
* Construct a metric hook with {@link OpenTelemetry} instance and options for the hook.
69+
*/
70+
public MetricsHook(final OpenTelemetry openTelemetry, final MetricHookOptions options) {
5971
final Meter meter = openTelemetry.getMeter(METER_NAME);
6072

6173
activeFlagEvaluationsCounter =
@@ -76,7 +88,8 @@ public MetricsHook(final OpenTelemetry openTelemetry, final List<DimensionDescri
7688
.setDescription("feature flag evaluation error counter")
7789
.build();
7890

79-
dimensionDescriptions = Collections.unmodifiableList(dimensions);
91+
dimensionDescriptions = Collections.unmodifiableList(options.getSetDimensions());
92+
extractor = options.getAttributeSetter();
8093
}
8194

8295

@@ -110,6 +123,10 @@ public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
110123
attributesBuilder.putAll(attributesFromFlagMetadata(details.getFlagMetadata(), dimensionDescriptions));
111124
}
112125

126+
if (extractor != null) {
127+
attributesBuilder.putAll(extractor.apply(details.getFlagMetadata()));
128+
}
129+
113130
evaluationSuccessCounter.add(+1, attributesBuilder.build());
114131
}
115132

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import dev.openfeature.sdk.FlagEvaluationDetails;
44
import dev.openfeature.sdk.Hook;
55
import dev.openfeature.sdk.HookContext;
6+
import dev.openfeature.sdk.ImmutableMetadata;
67
import io.opentelemetry.api.common.Attributes;
8+
import io.opentelemetry.api.common.AttributesBuilder;
79
import io.opentelemetry.api.trace.Span;
810
import io.opentelemetry.api.trace.StatusCode;
911

1012
import java.util.Map;
13+
import java.util.function.Function;
1114

1215
import static dev.openfeature.contrib.hooks.otel.OTelCommons.EVENT_NAME;
1316
import static dev.openfeature.contrib.hooks.otel.OTelCommons.flagKeyAttributeKey;
@@ -20,6 +23,7 @@
2023
*/
2124
public class TracesHook implements Hook {
2225
private final boolean setSpanErrorStatus;
26+
private final Function<ImmutableMetadata, Attributes> extractor;
2327

2428
/**
2529
* Create a new OpenTelemetryHook instance with default options.
@@ -33,6 +37,7 @@ public TracesHook() {
3337
*/
3438
public TracesHook(TracesHookOptions options) {
3539
setSpanErrorStatus = options.isSetSpanErrorStatus();
40+
extractor = options.getDimensionExtractor();
3641
}
3742

3843
/**
@@ -51,9 +56,18 @@ public TracesHook(TracesHookOptions options) {
5156
}
5257

5358
String variant = details.getVariant() != null ? details.getVariant() : String.valueOf(details.getValue());
54-
Attributes attributes = Attributes.of(flagKeyAttributeKey, ctx.getFlagKey(), providerNameAttributeKey,
55-
ctx.getProviderMetadata().getName(), variantAttributeKey, variant);
56-
currentSpan.addEvent(EVENT_NAME, attributes);
59+
60+
final AttributesBuilder attributesBuilder = Attributes.builder();
61+
62+
attributesBuilder.put(flagKeyAttributeKey, ctx.getFlagKey());
63+
attributesBuilder.put(providerNameAttributeKey, ctx.getProviderMetadata().getName());
64+
attributesBuilder.put(variantAttributeKey, variant);
65+
66+
if (extractor != null) {
67+
attributesBuilder.putAll(extractor.apply(details.getFlagMetadata()));
68+
}
69+
70+
currentSpan.addEvent(EVENT_NAME, attributesBuilder.build());
5771
}
5872

5973
/**

hooks/open-telemetry/src/main/java/dev/openfeature/contrib/hooks/otel/TracesHookOptions.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package dev.openfeature.contrib.hooks.otel;
22

3+
import dev.openfeature.sdk.ImmutableMetadata;
4+
import io.opentelemetry.api.common.Attributes;
35
import lombok.Builder;
46
import lombok.Getter;
57

8+
import java.util.function.Function;
9+
610
/**
7-
* OpenTelemetry hook options.
11+
* OpenTelemetry {@link TracesHook} options.
812
*/
913
@Builder
1014
@Getter
@@ -14,4 +18,10 @@ public class TracesHookOptions {
1418
*/
1519
@Builder.Default
1620
private boolean setSpanErrorStatus = false;
21+
22+
/**
23+
* Custom callback to derive {@link Attributes} from flag evaluation metadata represented with
24+
* {@link ImmutableMetadata}.
25+
*/
26+
private Function<ImmutableMetadata, Attributes> dimensionExtractor;
1727
}

hooks/open-telemetry/src/test/java/dev/openfeature/contrib/hooks/otel/MetricsHookTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,59 @@ public void finally_stage_validation() {
200200

201201
assertThat(attributes.get(flagKeyAttributeKey)).isEqualTo("key");
202202
}
203+
204+
@Test
205+
public void hook_option_validation(){
206+
// given
207+
MetricHookOptions hookOptions = MetricHookOptions.builder()
208+
.attributeSetter(metadata -> Attributes.builder()
209+
.put("boolean", metadata.getBoolean("boolean"))
210+
.put("integer", metadata.getInteger("integer"))
211+
.put("long", metadata.getLong("long"))
212+
.put("float", metadata.getFloat("float"))
213+
.put("double", metadata.getDouble("double"))
214+
.put("string", metadata.getString("string"))
215+
.build())
216+
.build();
217+
218+
final MetricsHook metricHook = new MetricsHook(telemetryExtension.getOpenTelemetry(), hookOptions);
219+
220+
final ImmutableMetadata metadata = ImmutableMetadata.builder()
221+
.addBoolean("boolean", true)
222+
.addInteger("integer", 1)
223+
.addLong("long", 1L)
224+
.addFloat("float", 1.0F)
225+
.addDouble("double", 1.0D)
226+
.addString("string", "string")
227+
.build();
228+
229+
final FlagEvaluationDetails<String> evaluationDetails = FlagEvaluationDetails.<String>builder()
230+
.flagKey("key")
231+
.value("value")
232+
.variant("variant")
233+
.reason("STATIC")
234+
.flagMetadata(metadata)
235+
.build();
236+
237+
// when
238+
metricHook.after(commonHookContext, evaluationDetails, null);
239+
List<MetricData> metrics = telemetryExtension.getMetrics();
240+
241+
// then
242+
assertThat(metrics).hasSize(1);
243+
244+
final MetricData metricData = metrics.get(0);
245+
final Optional<LongPointData> pointData = metricData.getLongSumData().getPoints().stream().findFirst();
246+
assertThat(pointData).isPresent();
247+
248+
final LongPointData longPointData = pointData.get();
249+
final Attributes attributes = longPointData.getAttributes();
250+
251+
assertThat(attributes.get(AttributeKey.stringKey("string"))).isEqualTo("string");
252+
assertThat(attributes.get(AttributeKey.doubleKey("double"))).isEqualTo(1.0D);
253+
assertThat(attributes.get(AttributeKey.doubleKey("float"))).isEqualTo(1.0F);
254+
assertThat(attributes.get(AttributeKey.longKey("long"))).isEqualTo(1L);
255+
assertThat(attributes.get(AttributeKey.longKey("integer"))).isEqualTo(1);
256+
assertThat(attributes.get(AttributeKey.booleanKey("boolean"))).isEqualTo(true);
257+
}
203258
}

hooks/open-telemetry/src/test/java/dev/openfeature/contrib/hooks/otel/TracesHookTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import dev.openfeature.sdk.FlagEvaluationDetails;
44
import dev.openfeature.sdk.FlagValueType;
55
import dev.openfeature.sdk.HookContext;
6+
import dev.openfeature.sdk.ImmutableMetadata;
67
import dev.openfeature.sdk.MutableContext;
78
import io.opentelemetry.api.common.AttributeKey;
89
import io.opentelemetry.api.common.Attributes;
10+
import io.opentelemetry.api.common.AttributesBuilder;
911
import io.opentelemetry.api.trace.Span;
1012
import org.junit.jupiter.api.AfterAll;
1113
import org.junit.jupiter.api.BeforeAll;
@@ -159,4 +161,50 @@ void should_not_call_record_exception_when_no_active_span() {
159161
verifyNoInteractions(span);
160162
}
161163

164+
@Test
165+
@DisplayName("should execute callback which populate span attributes")
166+
void should_execute_callback_which_populate_span_attributes() {
167+
FlagEvaluationDetails<String> details = FlagEvaluationDetails.<String>builder()
168+
.variant("test_variant")
169+
.value("variant_value")
170+
.flagMetadata(ImmutableMetadata.builder()
171+
.addBoolean("boolean", true)
172+
.addInteger("integer", 1)
173+
.addLong("long", 1L)
174+
.addFloat("float", 1.0F)
175+
.addDouble("double", 1.0D)
176+
.addString("string", "string")
177+
.build())
178+
.build();
179+
mockedSpan.when(Span::current).thenReturn(span);
180+
181+
TracesHookOptions options = TracesHookOptions.builder()
182+
.dimensionExtractor(metadata -> Attributes.builder()
183+
.put("boolean", metadata.getBoolean("boolean"))
184+
.put("integer", metadata.getInteger("integer"))
185+
.put("long", metadata.getLong("long"))
186+
.put("float", metadata.getFloat("float"))
187+
.put("double", metadata.getDouble("double"))
188+
.put("string", metadata.getString("string"))
189+
.build()
190+
).build();
191+
192+
TracesHook tracesHook = new TracesHook(options);
193+
tracesHook.after(hookContext, details, null);
194+
195+
final AttributesBuilder attributesBuilder = Attributes.builder();
196+
attributesBuilder.put(flagKeyAttributeKey, "test_key");
197+
attributesBuilder.put(providerNameAttributeKey, "test provider");
198+
attributesBuilder.put(variantAttributeKey, "test_variant");
199+
attributesBuilder.put("boolean", true);
200+
attributesBuilder.put("integer", 1);
201+
attributesBuilder.put("long", 1L);
202+
attributesBuilder.put("float", 1.0F);
203+
attributesBuilder.put("double", 1.0D);
204+
attributesBuilder.put("string", "string");
205+
206+
207+
verify(span).addEvent("feature_flag", attributesBuilder.build());
208+
}
209+
162210
}

0 commit comments

Comments
 (0)