From 05961de00edc224cba245717517174ff825ab272 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:46:11 +0000 Subject: [PATCH 1/2] feat: Update OpenTelemetry semantic conventions - Update attribute names to match latest semantic conventions: - feature_flag.provider_name -> feature_flag.provider.name - feature_flag.variant -> feature_flag.result.value - feature_flag.context.key -> feature_flag.context.id - Add new optional attributes: - feature_flag.result.reason.inExperiment - feature_flag.result.variationIndex - Deprecate withVariant() method, add withValue() for backward compatibility - Update tests to use new constant names and expect new attributes Follows specification: https://github.com/launchdarkly/sdk-specs/blob/main/specs/OTEL-openteletry-integration/README.md Reference implementations: dotnet-core#148, go-server-sdk#292 Co-Authored-By: rlamb@launchdarkly.com --- .../integrations/TracingHook.java | 49 +++++++++++++------ .../integrations/TracingHookTest.java | 20 ++++---- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java b/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java index 26e96ec..2ec720f 100644 --- a/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java +++ b/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java @@ -23,24 +23,26 @@ public class TracingHook extends Hook { static final String INSTRUMENTATION_NAME = "launchdarkly-client"; static final String DATA_KEY_SPAN = "variationSpan"; static final String EVENT_NAME = "feature_flag"; - static final String SEMCONV_FEATURE_FLAG_PROVIDER_NAME = "feature_flag.provider_name"; + static final String SEMCONV_FEATURE_FLAG_PROVIDER_NAME = "feature_flag.provider.name"; static final String SEMCONV_FEATURE_FLAG_KEY = "feature_flag.key"; - static final String SEMCONV_FEATURE_FLAG_VARIANT = "feature_flag.variant"; - static final String CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME = "feature_flag.context.key"; + static final String SEMCONV_FEATURE_FLAG_VALUE = "feature_flag.result.value"; + static final String SEMCONV_FEATURE_FLAG_CONTEXT_ID = "feature_flag.context.id"; + static final String SEMCONV_FEATURE_FLAG_VARIATION_INDEX = "feature_flag.result.variationIndex"; + static final String SEMCONV_FEATURE_FLAG_IN_EXPERIMENT = "feature_flag.result.reason.inExperiment"; private final boolean withSpans; - private final boolean withVariant; + private final boolean withValue; /** * Creates a {@link TracingHook} * * @param withSpans will include child spans for the various hook series when they happen - * @param withVariant will include the variant of the feature flag in the recorded evaluation events + * @param withValue will include the value of the feature flag in the recorded evaluation events */ - TracingHook(boolean withSpans, boolean withVariant) { + TracingHook(boolean withSpans, boolean withValue) { super(HOOK_NAME); this.withSpans = withSpans; - this.withVariant = withVariant; + this.withValue = withValue; } @Override @@ -59,7 +61,7 @@ Map beforeEvaluationInternal(Tracer tracer, EvaluationSeriesCont AttributesBuilder attrBuilder = Attributes.builder(); attrBuilder.put(SEMCONV_FEATURE_FLAG_KEY, seriesContext.flagKey); - attrBuilder.put(SEMCONV_FEATURE_FLAG_PROVIDER_NAME, PROVIDER_NAME); + attrBuilder.put(SEMCONV_FEATURE_FLAG_CONTEXT_ID, seriesContext.context.getFullyQualifiedKey()); builder.setAllAttributes(attrBuilder.build()); Span span = builder.startSpan(); Map retSeriesData = new HashMap<>(seriesData); @@ -78,9 +80,17 @@ public Map afterEvaluation(EvaluationSeriesContext seriesContext AttributesBuilder attrBuilder = Attributes.builder(); attrBuilder.put(SEMCONV_FEATURE_FLAG_KEY, seriesContext.flagKey); attrBuilder.put(SEMCONV_FEATURE_FLAG_PROVIDER_NAME, PROVIDER_NAME); - attrBuilder.put(CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME, seriesContext.context.getFullyQualifiedKey()); - if (withVariant) { - attrBuilder.put(SEMCONV_FEATURE_FLAG_VARIANT, evaluationDetail.getValue().toJsonString()); + attrBuilder.put(SEMCONV_FEATURE_FLAG_CONTEXT_ID, seriesContext.context.getFullyQualifiedKey()); + if (withValue) { + attrBuilder.put(SEMCONV_FEATURE_FLAG_VALUE, evaluationDetail.getValue().toJsonString()); + } + + if (evaluationDetail.getReason().isInExperiment()) { + attrBuilder.put(SEMCONV_FEATURE_FLAG_IN_EXPERIMENT, true); + } + + if (evaluationDetail.getVariationIndex() != EvaluationDetail.NO_VARIATION) { + attrBuilder.put(SEMCONV_FEATURE_FLAG_VARIATION_INDEX, evaluationDetail.getVariationIndex()); } // Here we make best effort the log the event and let the library handle the "no current span" case; which at the @@ -94,7 +104,7 @@ public Map afterEvaluation(EvaluationSeriesContext seriesContext */ public static class Builder { private boolean withSpans = false; - private boolean withVariant = false; + private boolean withValue = false; /** * The {@link TracingHook} will include child spans for the various hook series when they happen @@ -105,12 +115,23 @@ public Builder withSpans() { return this; } + /** + * The {@link TracingHook} will include the value of the feature flag in the recorded evaluation events + * @return the builder + */ + public Builder withValue() { + this.withValue = true; + return this; + } + /** * The {@link TracingHook} will include the variant of the feature flag in the recorded evaluation events * @return the builder + * @deprecated Use {@link #withValue()} instead */ + @Deprecated public Builder withVariant() { - this.withVariant = true; + this.withValue = true; return this; } @@ -118,7 +139,7 @@ public Builder withVariant() { * @return the {@link TracingHook} */ public TracingHook build() { - return new TracingHook(withSpans, withVariant); + return new TracingHook(withSpans, withValue); } } } diff --git a/lib/java-server-sdk-otel/src/test/java/com/launchdarkly/integrations/TracingHookTest.java b/lib/java-server-sdk-otel/src/test/java/com/launchdarkly/integrations/TracingHookTest.java index 49837f9..c5e0839 100644 --- a/lib/java-server-sdk-otel/src/test/java/com/launchdarkly/integrations/TracingHookTest.java +++ b/lib/java-server-sdk-otel/src/test/java/com/launchdarkly/integrations/TracingHookTest.java @@ -20,11 +20,11 @@ import java.util.List; import java.util.Map; -import static com.launchdarkly.integrations.TracingHook.CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME; +import static com.launchdarkly.integrations.TracingHook.SEMCONV_FEATURE_FLAG_CONTEXT_ID; import static com.launchdarkly.integrations.TracingHook.PROVIDER_NAME; import static com.launchdarkly.integrations.TracingHook.SEMCONV_FEATURE_FLAG_KEY; import static com.launchdarkly.integrations.TracingHook.SEMCONV_FEATURE_FLAG_PROVIDER_NAME; -import static com.launchdarkly.integrations.TracingHook.SEMCONV_FEATURE_FLAG_VARIANT; +import static com.launchdarkly.integrations.TracingHook.SEMCONV_FEATURE_FLAG_VALUE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -59,11 +59,11 @@ public void testAddsEventToParentSpanWtihoutVariation() { assertEquals(1, spanData.getEvents().size()); Attributes attributes = spanData.getEvents().get(0).getAttributes(); - assertEquals(3, attributes.size()); + assertEquals(4, attributes.size()); assertEquals(PROVIDER_NAME, attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_PROVIDER_NAME))); assertEquals("testKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_KEY))); - assertNull(attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_VARIANT))); - assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME))); + assertNull(attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_VALUE))); + assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_CONTEXT_ID))); } @Test @@ -90,11 +90,11 @@ public void testAddsEventToParentSpanWtihVariation() { assertEquals(1, spanData.getEvents().size()); Attributes attributes = spanData.getEvents().get(0).getAttributes(); - assertEquals(4, attributes.size()); + assertEquals(5, attributes.size()); assertEquals(PROVIDER_NAME, attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_PROVIDER_NAME))); assertEquals("testKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_KEY))); - assertEquals("{\"evalKey\":\"evalValue\"}", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_VARIANT))); - assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME))); + assertEquals("{\"evalKey\":\"evalValue\"}", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_VALUE))); + assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_CONTEXT_ID))); } @Test @@ -118,10 +118,10 @@ public void testCreatesChildSpanEventStillOnParent() { assertEquals("LDClient.testMethod", spanDataList.get(0).getName()); assertEquals("rootSpan", spanDataList.get(1).getName()); Attributes attributes = spanDataList.get(1).getEvents().get(0).getAttributes(); - assertEquals(3, attributes.size()); + assertEquals(4, attributes.size()); assertEquals("testKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_KEY))); assertEquals(PROVIDER_NAME, attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_PROVIDER_NAME))); - assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(CUSTOM_CONTEXT_KEY_ATTRIBUTE_NAME))); + assertEquals("testContextKey", attributes.get(AttributeKey.stringKey(SEMCONV_FEATURE_FLAG_CONTEXT_ID))); } @Test From f282f43992a845aff46d45c1335673f8656d5b30 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:30:37 +0000 Subject: [PATCH 2/2] fix: Add provider name attribute to beforeEvaluationInternal Restored SEMCONV_FEATURE_FLAG_PROVIDER_NAME attribute in beforeEvaluationInternal that was accidentally removed. The provider name should be included in both the span attributes (beforeEvaluationInternal) and event attributes (afterEvaluation). Addresses PR feedback from tanderson-ld and kinyoklion. Co-Authored-By: rlamb@launchdarkly.com --- .../src/main/java/com/launchdarkly/integrations/TracingHook.java | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java b/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java index 2ec720f..236dc08 100644 --- a/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java +++ b/lib/java-server-sdk-otel/src/main/java/com/launchdarkly/integrations/TracingHook.java @@ -61,6 +61,7 @@ Map beforeEvaluationInternal(Tracer tracer, EvaluationSeriesCont AttributesBuilder attrBuilder = Attributes.builder(); attrBuilder.put(SEMCONV_FEATURE_FLAG_KEY, seriesContext.flagKey); + attrBuilder.put(SEMCONV_FEATURE_FLAG_PROVIDER_NAME, PROVIDER_NAME); attrBuilder.put(SEMCONV_FEATURE_FLAG_CONTEXT_ID, seriesContext.context.getFullyQualifiedKey()); builder.setAllAttributes(attrBuilder.build()); Span span = builder.startSpan();