From 04ae0fb478124978b8030f0338f0724523545d5e Mon Sep 17 00:00:00 2001 From: typotter Date: Wed, 1 Apr 2026 16:59:27 -0600 Subject: [PATCH 01/15] Add flag evaluation metrics via OTel counter and OpenFeature Hook Record a `feature_flag.evaluations` OTel counter on every flag evaluation using an OpenFeature `finallyAfter` hook. The hook captures all evaluation paths including type mismatches that occur above the provider level. Attributes: feature_flag.key, feature_flag.result.variant, feature_flag.result.reason, error.type (on error), feature_flag.result.allocation_key (when present). Counter is a no-op when DD_METRICS_OTEL_ENABLED is false or opentelemetry-api is absent from the classpath. --- .../feature-flagging-api/build.gradle.kts | 2 + .../trace/api/openfeature/FlagEvalHook.java | 40 +++++ .../api/openfeature/FlagEvalMetrics.java | 80 +++++++++ .../trace/api/openfeature/Provider.java | 13 ++ .../api/openfeature/FlagEvalHookTest.java | 126 ++++++++++++++ .../api/openfeature/FlagEvalMetricsTest.java | 158 ++++++++++++++++++ .../trace/api/openfeature/ProviderTest.java | 24 +++ 7 files changed, 443 insertions(+) create mode 100644 products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java create mode 100644 products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java create mode 100644 products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalHookTest.java create mode 100644 products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index df475db801a..def6a16da8c 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -44,8 +44,10 @@ dependencies { api("dev.openfeature:sdk:1.20.1") compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap")) + compileOnly("io.opentelemetry:opentelemetry-api:1.47.0") testImplementation(project(":products:feature-flagging:feature-flagging-bootstrap")) + testImplementation("io.opentelemetry:opentelemetry-api:1.47.0") testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) testImplementation(libs.moshi) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java new file mode 100644 index 00000000000..93859ebf407 --- /dev/null +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java @@ -0,0 +1,40 @@ +package datadog.trace.api.openfeature; + +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.Hook; +import dev.openfeature.sdk.HookContext; +import dev.openfeature.sdk.ImmutableMetadata; +import java.util.Map; + +class FlagEvalHook implements Hook { + + private final FlagEvalMetrics metrics; + + FlagEvalHook(FlagEvalMetrics metrics) { + this.metrics = metrics; + } + + @Override + public void finallyAfter( + HookContext ctx, FlagEvaluationDetails details, Map hints) { + if (metrics == null) { + return; + } + try { + String flagKey = details.getFlagKey(); + String variant = details.getVariant(); + String reason = details.getReason(); + dev.openfeature.sdk.ErrorCode errorCode = details.getErrorCode(); + + String allocationKey = null; + ImmutableMetadata metadata = details.getFlagMetadata(); + if (metadata != null) { + allocationKey = metadata.getString("allocationKey"); + } + + metrics.record(flagKey, variant, reason, errorCode, allocationKey); + } catch (Exception e) { + // Never let metrics recording break flag evaluation + } + } +} diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java new file mode 100644 index 00000000000..ce3f174fc47 --- /dev/null +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -0,0 +1,80 @@ +package datadog.trace.api.openfeature; + +import dev.openfeature.sdk.ErrorCode; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; + +class FlagEvalMetrics { + + private static final String METER_NAME = "ddtrace.openfeature"; + private static final String METRIC_NAME = "feature_flag.evaluations"; + private static final String METRIC_UNIT = "{evaluation}"; + private static final String METRIC_DESC = "Number of feature flag evaluations"; + + private static final AttributeKey ATTR_FLAG_KEY = + AttributeKey.stringKey("feature_flag.key"); + private static final AttributeKey ATTR_VARIANT = + AttributeKey.stringKey("feature_flag.result.variant"); + private static final AttributeKey ATTR_REASON = + AttributeKey.stringKey("feature_flag.result.reason"); + private static final AttributeKey ATTR_ERROR_TYPE = AttributeKey.stringKey("error.type"); + private static final AttributeKey ATTR_ALLOCATION_KEY = + AttributeKey.stringKey("feature_flag.result.allocation_key"); + + private volatile LongCounter counter; + + FlagEvalMetrics() { + try { + Meter meter = GlobalOpenTelemetry.getMeterProvider().meterBuilder(METER_NAME).build(); + counter = + meter + .counterBuilder(METRIC_NAME) + .setUnit(METRIC_UNIT) + .setDescription(METRIC_DESC) + .build(); + } catch (NoClassDefFoundError | Exception e) { + // OTel API not on classpath or initialization failed — counter stays null (no-op) + counter = null; + } + } + + /** Package-private constructor for testing with a mock counter. */ + FlagEvalMetrics(LongCounter counter) { + this.counter = counter; + } + + void record( + String flagKey, String variant, String reason, ErrorCode errorCode, String allocationKey) { + LongCounter c = counter; + if (c == null) { + return; + } + try { + AttributesBuilder builder = + Attributes.builder() + .put(ATTR_FLAG_KEY, flagKey) + .put(ATTR_VARIANT, variant != null ? variant : "") + .put(ATTR_REASON, reason != null ? reason.toLowerCase() : "unknown"); + + if (errorCode != null) { + builder.put(ATTR_ERROR_TYPE, errorCode.name().toLowerCase()); + } + + if (allocationKey != null && !allocationKey.isEmpty()) { + builder.put(ATTR_ALLOCATION_KEY, allocationKey); + } + + c.add(1, builder.build()); + } catch (Exception e) { + // Never let metrics recording break flag evaluation + } + } + + void shutdown() { + counter = null; + } +} diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 0b0faf38c1c..bc62aaccfa7 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -5,6 +5,7 @@ import de.thetaphi.forbiddenapis.SuppressForbidden; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.EventProvider; +import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.Metadata; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEvent; @@ -14,6 +15,8 @@ import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -25,6 +28,8 @@ public class Provider extends EventProvider implements Metadata { private volatile Evaluator evaluator; private final Options options; private final AtomicBoolean initialized = new AtomicBoolean(false); + private final FlagEvalMetrics flagEvalMetrics; + private final FlagEvalHook flagEvalHook; public Provider() { this(DEFAULT_OPTIONS, null); @@ -37,6 +42,8 @@ public Provider(final Options options) { Provider(final Options options, final Evaluator evaluator) { this.options = options; this.evaluator = evaluator; + this.flagEvalMetrics = new FlagEvalMetrics(); + this.flagEvalHook = new FlagEvalHook(flagEvalMetrics); } @Override @@ -77,8 +84,14 @@ private Evaluator buildEvaluator() throws Exception { return (Evaluator) ctor.newInstance((Runnable) this::onConfigurationChange); } + @Override + public List getProviderHooks() { + return Collections.singletonList(flagEvalHook); + } + @Override public void shutdown() { + flagEvalMetrics.shutdown(); if (evaluator != null) { evaluator.shutdown(); } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalHookTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalHookTest.java new file mode 100644 index 00000000000..8ed17d91cbb --- /dev/null +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalHookTest.java @@ -0,0 +1,126 @@ +package datadog.trace.api.openfeature; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.ImmutableMetadata; +import dev.openfeature.sdk.Reason; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class FlagEvalHookTest { + + @Test + void finallyAfterRecordsBasicEvaluation() { + FlagEvalMetrics metrics = mock(FlagEvalMetrics.class); + FlagEvalHook hook = new FlagEvalHook(metrics); + + FlagEvaluationDetails details = + FlagEvaluationDetails.builder() + .flagKey("my-flag") + .value("on-value") + .variant("on") + .reason(Reason.TARGETING_MATCH.name()) + .flagMetadata( + ImmutableMetadata.builder().addString("allocationKey", "default-alloc").build()) + .build(); + + hook.finallyAfter(null, details, Collections.emptyMap()); + + verify(metrics) + .record( + eq("my-flag"), + eq("on"), + eq(Reason.TARGETING_MATCH.name()), + isNull(), + eq("default-alloc")); + } + + @Test + void finallyAfterRecordsErrorEvaluation() { + FlagEvalMetrics metrics = mock(FlagEvalMetrics.class); + FlagEvalHook hook = new FlagEvalHook(metrics); + + FlagEvaluationDetails details = + FlagEvaluationDetails.builder() + .flagKey("missing-flag") + .value("default") + .reason(Reason.ERROR.name()) + .errorCode(ErrorCode.FLAG_NOT_FOUND) + .build(); + + hook.finallyAfter(null, details, Collections.emptyMap()); + + verify(metrics) + .record( + eq("missing-flag"), + isNull(), + eq(Reason.ERROR.name()), + eq(ErrorCode.FLAG_NOT_FOUND), + isNull()); + } + + @Test + void finallyAfterHandlesNullFlagMetadata() { + FlagEvalMetrics metrics = mock(FlagEvalMetrics.class); + FlagEvalHook hook = new FlagEvalHook(metrics); + + FlagEvaluationDetails details = + FlagEvaluationDetails.builder() + .flagKey("my-flag") + .value(true) + .variant("on") + .reason(Reason.TARGETING_MATCH.name()) + .build(); + + hook.finallyAfter(null, details, Collections.emptyMap()); + + verify(metrics) + .record(eq("my-flag"), eq("on"), eq(Reason.TARGETING_MATCH.name()), isNull(), isNull()); + } + + @Test + void finallyAfterHandlesNullVariantAndReason() { + FlagEvalMetrics metrics = mock(FlagEvalMetrics.class); + FlagEvalHook hook = new FlagEvalHook(metrics); + + FlagEvaluationDetails details = + FlagEvaluationDetails.builder().flagKey("my-flag").value("default").build(); + + hook.finallyAfter(null, details, Collections.emptyMap()); + + verify(metrics).record(eq("my-flag"), isNull(), isNull(), isNull(), isNull()); + } + + @Test + void finallyAfterNeverThrows() { + FlagEvalMetrics metrics = mock(FlagEvalMetrics.class); + FlagEvalHook hook = new FlagEvalHook(metrics); + + // Should not throw even with completely null inputs + hook.finallyAfter(null, null, null); + + verifyNoInteractions(metrics); + } + + @Test + void finallyAfterIsNoOpWhenMetricsIsNull() { + FlagEvalHook hook = new FlagEvalHook(null); + + FlagEvaluationDetails details = + FlagEvaluationDetails.builder() + .flagKey("my-flag") + .value(true) + .variant("on") + .reason(Reason.TARGETING_MATCH.name()) + .build(); + + // Should not throw + hook.finallyAfter(null, details, Collections.emptyMap()); + } +} diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java new file mode 100644 index 00000000000..13261f366fe --- /dev/null +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java @@ -0,0 +1,158 @@ +package datadog.trace.api.openfeature; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import dev.openfeature.sdk.ErrorCode; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class FlagEvalMetricsTest { + + @Test + void recordBasicAttributes() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", "on", "TARGETING_MATCH", null, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "feature_flag.key", "my-flag"); + assertAttribute(attrs, "feature_flag.result.variant", "on"); + assertAttribute(attrs, "feature_flag.result.reason", "targeting_match"); + assertNoAttribute(attrs, "error.type"); + assertNoAttribute(attrs, "feature_flag.result.allocation_key"); + } + + @Test + void recordErrorAttributes() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("missing-flag", "", "ERROR", ErrorCode.FLAG_NOT_FOUND, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "feature_flag.key", "missing-flag"); + assertAttribute(attrs, "feature_flag.result.variant", ""); + assertAttribute(attrs, "feature_flag.result.reason", "error"); + assertAttribute(attrs, "error.type", "flag_not_found"); + assertNoAttribute(attrs, "feature_flag.result.allocation_key"); + } + + @Test + void recordTypeMismatchError() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", "", "ERROR", ErrorCode.TYPE_MISMATCH, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "error.type", "type_mismatch"); + } + + @Test + void recordWithAllocationKey() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", "on", "TARGETING_MATCH", null, "default-allocation"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "feature_flag.result.allocation_key", "default-allocation"); + } + + @Test + void recordOmitsEmptyAllocationKey() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", "on", "TARGETING_MATCH", null, ""); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertNoAttribute(attrs, "feature_flag.result.allocation_key"); + } + + @Test + void recordNullVariantBecomesEmptyString() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", null, "DEFAULT", null, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "feature_flag.result.variant", ""); + } + + @Test + void recordNullReasonBecomesUnknown() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.record("my-flag", "on", null, null, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Attributes.class); + verify(counter).add(eq(1L), captor.capture()); + + Attributes attrs = captor.getValue(); + assertAttribute(attrs, "feature_flag.result.reason", "unknown"); + } + + @Test + void recordIsNoOpWhenCounterIsNull() { + FlagEvalMetrics metrics = new FlagEvalMetrics(null); + // Should not throw + metrics.record("my-flag", "on", "TARGETING_MATCH", null, null); + } + + @Test + void shutdownClearsCounter() { + LongCounter counter = mock(LongCounter.class); + FlagEvalMetrics metrics = new FlagEvalMetrics(counter); + + metrics.shutdown(); + metrics.record("my-flag", "on", "TARGETING_MATCH", null, null); + + verifyNoInteractions(counter); + } + + private static void assertAttribute(Attributes attrs, String key, String expected) { + String value = + attrs.asMap().entrySet().stream() + .filter(e -> e.getKey().getKey().equals(key)) + .map(e -> e.getValue().toString()) + .findFirst() + .orElse(null); + if (!expected.equals(value)) { + throw new AssertionError("Expected attribute " + key + "=" + expected + " but got " + value); + } + } + + private static void assertNoAttribute(Attributes attrs, String key) { + boolean present = attrs.asMap().keySet().stream().anyMatch(k -> k.getKey().equals(key)); + if (present) { + throw new AssertionError("Expected no attribute " + key + " but it was present"); + } + } +} diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 4ed1495bd00..87a80f59e20 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -24,6 +24,7 @@ import dev.openfeature.sdk.EventDetails; import dev.openfeature.sdk.Features; import dev.openfeature.sdk.FlagEvaluationDetails; +import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.OpenFeatureAPI; import dev.openfeature.sdk.ProviderEvaluation; import dev.openfeature.sdk.ProviderEvent; @@ -31,6 +32,7 @@ import dev.openfeature.sdk.Value; import dev.openfeature.sdk.exceptions.FatalError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -138,6 +140,28 @@ protected Class loadEvaluatorClass() throws ClassNotFoundException { })); } + @Test + public void testGetProviderHooksReturnsFlagEvalHook() { + Provider provider = + new Provider(new Options().initTimeout(10, MILLISECONDS), mock(Evaluator.class)); + List hooks = provider.getProviderHooks(); + assertThat(hooks.size(), equalTo(1)); + assertThat(hooks.get(0) instanceof FlagEvalHook, equalTo(true)); + } + + @Test + public void testShutdownCleansUpMetrics() throws Exception { + Evaluator evaluator = mock(Evaluator.class); + when(evaluator.initialize(anyLong(), any(), any())).thenReturn(true); + Provider provider = new Provider(new Options().initTimeout(10, MILLISECONDS), evaluator); + provider.initialize(null); + provider.shutdown(); + verify(evaluator).shutdown(); + // After shutdown, getProviderHooks still returns a list (hook is still present but metrics is + // shut down) + assertThat(provider.getProviderHooks().size(), equalTo(1)); + } + public interface EvaluateMethod { FlagEvaluationDetails evaluate(Features client, String flag, E defaultValue); } From c5467fb6e5b2afd373bbfd166792c309f10655c4 Mon Sep 17 00:00:00 2001 From: typotter Date: Wed, 1 Apr 2026 20:54:24 -0600 Subject: [PATCH 02/15] Use own SdkMeterProvider with OTLP HTTP exporter for eval metrics Replace GlobalOpenTelemetry.getMeterProvider() with a dedicated SdkMeterProvider + OtlpHttpMetricExporter that sends metrics directly to the DD Agent's OTLP endpoint (default :4318/v1/metrics). This avoids the agent's OTel class shading issue where the agent relocates io.opentelemetry.api.* to datadog.trace.bootstrap.otel.api.*, making GlobalOpenTelemetry calls from the dd-openfeature jar hit the unshaded no-op provider instead of the agent's shim. Requires opentelemetry-sdk-metrics and opentelemetry-exporter-otlp on the application classpath. Falls back to no-op if absent. System tests: 11/17 pass. 6 failures are pre-existing DDEvaluator gaps (reason mapping, parse errors, type mismatch strictness). --- .../feature-flagging-api/build.gradle.kts | 4 ++ .../api/openfeature/FlagEvalMetrics.java | 46 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index def6a16da8c..e630ec1e6b6 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -45,9 +45,13 @@ dependencies { compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap")) compileOnly("io.opentelemetry:opentelemetry-api:1.47.0") + compileOnly("io.opentelemetry:opentelemetry-sdk-metrics:1.47.0") + compileOnly("io.opentelemetry:opentelemetry-exporter-otlp:1.47.0") testImplementation(project(":products:feature-flagging:feature-flagging-bootstrap")) testImplementation("io.opentelemetry:opentelemetry-api:1.47.0") + testImplementation("io.opentelemetry:opentelemetry-sdk-metrics:1.47.0") + testImplementation("io.opentelemetry:opentelemetry-exporter-otlp:1.47.0") testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) testImplementation(libs.moshi) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index ce3f174fc47..c1eeab95e52 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -1,19 +1,27 @@ package datadog.trace.api.openfeature; import dev.openfeature.sdk.ErrorCode; -import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import java.io.Closeable; +import java.time.Duration; -class FlagEvalMetrics { +class FlagEvalMetrics implements Closeable { private static final String METER_NAME = "ddtrace.openfeature"; private static final String METRIC_NAME = "feature_flag.evaluations"; private static final String METRIC_UNIT = "{evaluation}"; private static final String METRIC_DESC = "Number of feature flag evaluations"; + private static final Duration EXPORT_INTERVAL = Duration.ofSeconds(10); + + private static final String DEFAULT_ENDPOINT = "http://localhost:4318/v1/metrics"; + private static final String ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"; private static final AttributeKey ATTR_FLAG_KEY = AttributeKey.stringKey("feature_flag.key"); @@ -26,10 +34,24 @@ class FlagEvalMetrics { AttributeKey.stringKey("feature_flag.result.allocation_key"); private volatile LongCounter counter; + private volatile SdkMeterProvider meterProvider; FlagEvalMetrics() { try { - Meter meter = GlobalOpenTelemetry.getMeterProvider().meterBuilder(METER_NAME).build(); + String endpoint = System.getenv(ENDPOINT_ENV); + if (endpoint == null || endpoint.isEmpty()) { + endpoint = DEFAULT_ENDPOINT; + } + + OtlpHttpMetricExporter exporter = + OtlpHttpMetricExporter.builder().setEndpoint(endpoint).build(); + + PeriodicMetricReader reader = + PeriodicMetricReader.builder(exporter).setInterval(EXPORT_INTERVAL).build(); + + meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + Meter meter = meterProvider.meterBuilder(METER_NAME).build(); counter = meter .counterBuilder(METRIC_NAME) @@ -37,14 +59,16 @@ class FlagEvalMetrics { .setDescription(METRIC_DESC) .build(); } catch (NoClassDefFoundError | Exception e) { - // OTel API not on classpath or initialization failed — counter stays null (no-op) + // OTel SDK not on classpath or initialization failed — counter stays null (no-op) counter = null; + meterProvider = null; } } /** Package-private constructor for testing with a mock counter. */ FlagEvalMetrics(LongCounter counter) { this.counter = counter; + this.meterProvider = null; } void record( @@ -74,7 +98,21 @@ void record( } } + @Override + public void close() { + shutdown(); + } + void shutdown() { counter = null; + SdkMeterProvider mp = meterProvider; + if (mp != null) { + meterProvider = null; + try { + mp.close(); + } catch (Exception e) { + // Ignore shutdown errors + } + } } } From 1816d301e1fd546693e1988001b2a1947ea92bbe Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 2 Apr 2026 00:25:20 -0600 Subject: [PATCH 03/15] Address code review feedback for eval metrics - Add explicit null guard for details in FlagEvalHook.finallyAfter() - Add OTEL_EXPORTER_OTLP_ENDPOINT generic env var fallback with /v1/metrics path appended (per OTel spec fallback chain) - Add comments clarifying signal-specific vs generic endpoint behavior --- .../datadog/trace/api/openfeature/FlagEvalHook.java | 2 +- .../datadog/trace/api/openfeature/FlagEvalMetrics.java | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java index 93859ebf407..8562db2b6cf 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java @@ -17,7 +17,7 @@ class FlagEvalHook implements Hook { @Override public void finallyAfter( HookContext ctx, FlagEvaluationDetails details, Map hints) { - if (metrics == null) { + if (metrics == null || details == null) { return; } try { diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index c1eeab95e52..15d9f50a07b 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -21,7 +21,10 @@ class FlagEvalMetrics implements Closeable { private static final Duration EXPORT_INTERVAL = Duration.ofSeconds(10); private static final String DEFAULT_ENDPOINT = "http://localhost:4318/v1/metrics"; + // Signal-specific env var (used as-is, must include /v1/metrics path) private static final String ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"; + // Generic env var fallback (base URL, /v1/metrics is appended) + private static final String ENDPOINT_GENERIC_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT"; private static final AttributeKey ATTR_FLAG_KEY = AttributeKey.stringKey("feature_flag.key"); @@ -40,7 +43,12 @@ class FlagEvalMetrics implements Closeable { try { String endpoint = System.getenv(ENDPOINT_ENV); if (endpoint == null || endpoint.isEmpty()) { - endpoint = DEFAULT_ENDPOINT; + String base = System.getenv(ENDPOINT_GENERIC_ENV); + if (base != null && !base.isEmpty()) { + endpoint = base.endsWith("/") ? base + "v1/metrics" : base + "/v1/metrics"; + } else { + endpoint = DEFAULT_ENDPOINT; + } } OtlpHttpMetricExporter exporter = From 3d789f00a442bf708260d4b42a63a63cd2c8221d Mon Sep 17 00:00:00 2001 From: typotter Date: Wed, 8 Apr 2026 13:25:05 -0600 Subject: [PATCH 04/15] Fix NoClassDefFoundError when OTel SDK absent from classpath When the OTel SDK jars are not on the application classpath, loading FlagEvalMetrics fails because field types reference OTel SDK classes (SdkMeterProvider). This propagated as an uncaught NoClassDefFoundError from the Provider constructor, crashing provider initialization. Fix: - Change meterProvider field type from SdkMeterProvider to Closeable (always on classpath), use local SdkMeterProvider variable inside try block - Catch NoClassDefFoundError in Provider constructor when creating FlagEvalMetrics - Null-safe getProviderHooks() and shutdown() when metrics is null --- .../trace/api/openfeature/FlagEvalMetrics.java | 12 ++++++++---- .../datadog/trace/api/openfeature/Provider.java | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 15d9f50a07b..1810ba353df 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -37,7 +37,9 @@ class FlagEvalMetrics implements Closeable { AttributeKey.stringKey("feature_flag.result.allocation_key"); private volatile LongCounter counter; - private volatile SdkMeterProvider meterProvider; + // Typed as Closeable to avoid loading SdkMeterProvider at class-load time + // when the OTel SDK is absent from the classpath + private volatile java.io.Closeable meterProvider; FlagEvalMetrics() { try { @@ -57,9 +59,11 @@ class FlagEvalMetrics implements Closeable { PeriodicMetricReader reader = PeriodicMetricReader.builder(exporter).setInterval(EXPORT_INTERVAL).build(); - meterProvider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().registerMetricReader(reader).build(); + meterProvider = sdkMeterProvider; - Meter meter = meterProvider.meterBuilder(METER_NAME).build(); + Meter meter = sdkMeterProvider.meterBuilder(METER_NAME).build(); counter = meter .counterBuilder(METRIC_NAME) @@ -113,7 +117,7 @@ public void close() { void shutdown() { counter = null; - SdkMeterProvider mp = meterProvider; + java.io.Closeable mp = meterProvider; if (mp != null) { meterProvider = null; try { diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index bc62aaccfa7..82b4f757ee6 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -42,8 +42,14 @@ public Provider(final Options options) { Provider(final Options options, final Evaluator evaluator) { this.options = options; this.evaluator = evaluator; - this.flagEvalMetrics = new FlagEvalMetrics(); - this.flagEvalHook = new FlagEvalHook(flagEvalMetrics); + FlagEvalMetrics metrics = null; + try { + metrics = new FlagEvalMetrics(); + } catch (NoClassDefFoundError | Exception e) { + // OTel classes not on classpath — metrics disabled + } + this.flagEvalMetrics = metrics; + this.flagEvalHook = new FlagEvalHook(metrics); } @Override @@ -86,12 +92,17 @@ private Evaluator buildEvaluator() throws Exception { @Override public List getProviderHooks() { + if (flagEvalHook == null) { + return Collections.emptyList(); + } return Collections.singletonList(flagEvalHook); } @Override public void shutdown() { - flagEvalMetrics.shutdown(); + if (flagEvalMetrics != null) { + flagEvalMetrics.shutdown(); + } if (evaluator != null) { evaluator.shutdown(); } From 69c552998a2107872f63cb53b15b412ceeabab95 Mon Sep 17 00:00:00 2001 From: typotter Date: Wed, 8 Apr 2026 16:09:46 -0600 Subject: [PATCH 05/15] Move FlagEvalHook construction inside try/catch block FlagEvalHook references FlagEvalMetrics in its field declaration. On JVMs that eagerly verify field types during class loading, constructing FlagEvalHook outside the try/catch could throw NoClassDefFoundError if OTel classes failed to load. Moving it inside the try block ensures both metrics and hook are null-safe when OTel is absent. --- .../src/main/java/datadog/trace/api/openfeature/Provider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 82b4f757ee6..d369a9ea3b3 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -43,13 +43,15 @@ public Provider(final Options options) { this.options = options; this.evaluator = evaluator; FlagEvalMetrics metrics = null; + FlagEvalHook hook = null; try { metrics = new FlagEvalMetrics(); + hook = new FlagEvalHook(metrics); } catch (NoClassDefFoundError | Exception e) { // OTel classes not on classpath — metrics disabled } this.flagEvalMetrics = metrics; - this.flagEvalHook = new FlagEvalHook(metrics); + this.flagEvalHook = hook; } @Override From 18c0441ef0e18e5dd875aa44037901b923dc10a4 Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 9 Apr 2026 09:34:33 -0600 Subject: [PATCH 06/15] Add README for dd-openfeature with eval metrics setup Documents the published artifact setup, evaluation metrics dependencies (opentelemetry-sdk-metrics, opentelemetry-exporter-otlp), OTLP endpoint configuration, metric attributes, and requirements. --- .../feature-flagging-api/README.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 products/feature-flagging/feature-flagging-api/README.md diff --git a/products/feature-flagging/feature-flagging-api/README.md b/products/feature-flagging/feature-flagging-api/README.md new file mode 100644 index 00000000000..31e68572a91 --- /dev/null +++ b/products/feature-flagging/feature-flagging-api/README.md @@ -0,0 +1,82 @@ +# dd-openfeature + +Datadog OpenFeature Provider for Java. Implements the [OpenFeature](https://openfeature.dev/) `FeatureProvider` interface for Datadog's Feature Flags and Experimentation (FFE) product. + +Published as `com.datadoghq:dd-openfeature` on Maven Central. + +## Setup + +```xml + + com.datadoghq + dd-openfeature + ${dd-openfeature.version} + + + dev.openfeature + sdk + 1.20.1 + +``` + +### Evaluation metrics (optional) + +To enable evaluation metrics (`feature_flag.evaluations` counter), add the OpenTelemetry SDK dependencies: + +```xml + + io.opentelemetry + opentelemetry-sdk-metrics + 1.47.0 + + + io.opentelemetry + opentelemetry-exporter-otlp + 1.47.0 + +``` + +Any OpenTelemetry API 1.x version is compatible. If these dependencies are absent, the provider operates normally without metrics. + +## Usage + +```java +import datadog.trace.api.openfeature.Provider; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Client; + +OpenFeatureAPI api = OpenFeatureAPI.getInstance(); +api.setProviderAndWait(new Provider()); +Client client = api.getClient(); + +boolean enabled = client.getBooleanValue("my-feature", false, + new MutableContext("user-123")); +``` + +## Evaluation metrics + +When the OTel SDK dependencies are on the classpath, the provider records a `feature_flag.evaluations` counter via OTLP HTTP/protobuf. Metrics are exported every 10 seconds to the Datadog Agent's OTLP receiver. + +### Configuration + +| Environment variable | Description | Default | +|---|---|---| +| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Signal-specific OTLP endpoint (used as-is) | — | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | Generic OTLP endpoint (`/v1/metrics` appended) | — | +| (none set) | Default endpoint | `http://localhost:4318/v1/metrics` | + +### Metric attributes + +| Attribute | Description | +|---|---| +| `feature_flag.key` | Flag key | +| `feature_flag.result.variant` | Resolved variant key | +| `feature_flag.result.reason` | Evaluation reason (lowercased) | +| `error.type` | Error code (lowercased, only on error) | +| `feature_flag.result.allocation_key` | Allocation key (when present) | + +## Requirements + +- Java 11+ +- Datadog Agent with Remote Configuration enabled +- `DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED=true` From da921984b2a210ac30717ebda139eb072ba8d7b1 Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 9 Apr 2026 11:42:55 -0600 Subject: [PATCH 07/15] Use ConfigHelper.env() instead of System.getenv() System.getenv() is forbidden by the project's forbiddenApis rules. Replace with ConfigHelper.env() which is the approved way to read environment variables. Add config-utils as compileOnly dependency. --- .../feature-flagging/feature-flagging-api/build.gradle.kts | 1 + .../java/datadog/trace/api/openfeature/FlagEvalMetrics.java | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index e630ec1e6b6..3999633bd1e 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { api("dev.openfeature:sdk:1.20.1") compileOnly(project(":products:feature-flagging:feature-flagging-bootstrap")) + compileOnly(project(":utils:config-utils")) compileOnly("io.opentelemetry:opentelemetry-api:1.47.0") compileOnly("io.opentelemetry:opentelemetry-sdk-metrics:1.47.0") compileOnly("io.opentelemetry:opentelemetry-exporter-otlp:1.47.0") diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 1810ba353df..0818dff24e0 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -1,5 +1,6 @@ package datadog.trace.api.openfeature; +import datadog.trace.config.inversion.ConfigHelper; import dev.openfeature.sdk.ErrorCode; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; @@ -43,9 +44,9 @@ class FlagEvalMetrics implements Closeable { FlagEvalMetrics() { try { - String endpoint = System.getenv(ENDPOINT_ENV); + String endpoint = ConfigHelper.env(ENDPOINT_ENV); if (endpoint == null || endpoint.isEmpty()) { - String base = System.getenv(ENDPOINT_GENERIC_ENV); + String base = ConfigHelper.env(ENDPOINT_GENERIC_ENV); if (base != null && !base.isEmpty()) { endpoint = base.endsWith("/") ? base + "v1/metrics" : base + "/v1/metrics"; } else { From 340f25c3f493249a790a2489674b2f0fee2c4414 Mon Sep 17 00:00:00 2001 From: typotter Date: Fri, 10 Apr 2026 13:15:07 -0600 Subject: [PATCH 08/15] Address PR review feedback from manuel-alvarez-alvarez - Remove transitive openfeature-sdk dep from README setup section - Import ErrorCode at top of FlagEvalHook instead of inline FQN --- products/feature-flagging/feature-flagging-api/README.md | 7 ++----- .../java/datadog/trace/api/openfeature/FlagEvalHook.java | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/README.md b/products/feature-flagging/feature-flagging-api/README.md index 31e68572a91..5d1beece1ca 100644 --- a/products/feature-flagging/feature-flagging-api/README.md +++ b/products/feature-flagging/feature-flagging-api/README.md @@ -12,13 +12,10 @@ Published as `com.datadoghq:dd-openfeature` on Maven Central. dd-openfeature ${dd-openfeature.version} - - dev.openfeature - sdk - 1.20.1 - ``` +The OpenFeature SDK (`dev.openfeature:sdk`) is included as a transitive dependency. + ### Evaluation metrics (optional) To enable evaluation metrics (`feature_flag.evaluations` counter), add the OpenTelemetry SDK dependencies: diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java index 8562db2b6cf..1132602a53f 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalHook.java @@ -1,5 +1,6 @@ package datadog.trace.api.openfeature; +import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.FlagEvaluationDetails; import dev.openfeature.sdk.Hook; import dev.openfeature.sdk.HookContext; @@ -24,7 +25,7 @@ public void finallyAfter( String flagKey = details.getFlagKey(); String variant = details.getVariant(); String reason = details.getReason(); - dev.openfeature.sdk.ErrorCode errorCode = details.getErrorCode(); + ErrorCode errorCode = details.getErrorCode(); String allocationKey = null; ImmutableMetadata metadata = details.getFlagMetadata(); From e7fcc47c0e28924a489fd0a5568cab483b09b6f3 Mon Sep 17 00:00:00 2001 From: typotter Date: Mon, 13 Apr 2026 15:36:56 -0600 Subject: [PATCH 09/15] Add evaluationLogging option and log errors when OTel SDK absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Options.evaluationLogging(boolean) — default true per EVALLOG.12 - When disabled: no metrics, no hook, no error - When enabled + OTel SDK missing: log.error with instructions to add deps or disable, degrade to no-op (matches Go/Python pattern) - When enabled + OTel init failure: log.error with message, degrade - Remove silent catch — FlagEvalMetrics now logs at error level for NoClassDefFoundError and at error level for other init failures --- .../api/openfeature/FlagEvalMetrics.java | 21 +++++++++++++---- .../trace/api/openfeature/Provider.java | 23 +++++++++++++++---- .../trace/api/openfeature/ProviderTest.java | 9 ++++++++ 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 0818dff24e0..6f185511446 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -12,9 +12,13 @@ import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import java.io.Closeable; import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class FlagEvalMetrics implements Closeable { + private static final Logger log = LoggerFactory.getLogger(FlagEvalMetrics.class); + private static final String METER_NAME = "ddtrace.openfeature"; private static final String METRIC_NAME = "feature_flag.evaluations"; private static final String METRIC_UNIT = "{evaluation}"; @@ -40,7 +44,7 @@ class FlagEvalMetrics implements Closeable { private volatile LongCounter counter; // Typed as Closeable to avoid loading SdkMeterProvider at class-load time // when the OTel SDK is absent from the classpath - private volatile java.io.Closeable meterProvider; + private volatile Closeable meterProvider; FlagEvalMetrics() { try { @@ -71,8 +75,17 @@ class FlagEvalMetrics implements Closeable { .setUnit(METRIC_UNIT) .setDescription(METRIC_DESC) .build(); - } catch (NoClassDefFoundError | Exception e) { - // OTel SDK not on classpath or initialization failed — counter stays null (no-op) + + log.debug("Flag evaluation metrics initialized, exporting to {}", endpoint); + } catch (NoClassDefFoundError e) { + log.error( + "Evaluation logging is enabled but OpenTelemetry SDK is not on the classpath. " + + "Add opentelemetry-sdk-metrics and opentelemetry-exporter-otlp to your dependencies, " + + "or disable evaluation logging via Provider.Options.evaluationLogging(false)."); + counter = null; + meterProvider = null; + } catch (Exception e) { + log.error("Failed to initialize flag evaluation metrics: {}", e.getMessage()); counter = null; meterProvider = null; } @@ -118,7 +131,7 @@ public void close() { void shutdown() { counter = null; - java.io.Closeable mp = meterProvider; + Closeable mp = meterProvider; if (mp != null) { meterProvider = null; try { diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index d369a9ea3b3..0d6da65a177 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -44,11 +44,13 @@ public Provider(final Options options) { this.evaluator = evaluator; FlagEvalMetrics metrics = null; FlagEvalHook hook = null; - try { - metrics = new FlagEvalMetrics(); - hook = new FlagEvalHook(metrics); - } catch (NoClassDefFoundError | Exception e) { - // OTel classes not on classpath — metrics disabled + if (options.isEvaluationLogging()) { + try { + metrics = new FlagEvalMetrics(); + hook = new FlagEvalHook(metrics); + } catch (NoClassDefFoundError | Exception e) { + // FlagEvalMetrics logs the error — degrade to no-op + } } this.flagEvalMetrics = metrics; this.flagEvalHook = hook; @@ -159,6 +161,7 @@ public static class Options { private long timeout; private TimeUnit unit; + private boolean evaluationLogging = true; public Options initTimeout(final long timeout, final TimeUnit unit) { this.timeout = timeout; @@ -166,6 +169,12 @@ public Options initTimeout(final long timeout, final TimeUnit unit) { return this; } + /** Enable or disable evaluation logging via OTel metrics. Default: true. */ + public Options evaluationLogging(final boolean enabled) { + this.evaluationLogging = enabled; + return this; + } + public long getTimeout() { return timeout; } @@ -173,5 +182,9 @@ public long getTimeout() { public TimeUnit getUnit() { return unit; } + + public boolean isEvaluationLogging() { + return evaluationLogging; + } } } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 87a80f59e20..6ec9bef6f4c 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -149,6 +149,15 @@ public void testGetProviderHooksReturnsFlagEvalHook() { assertThat(hooks.get(0) instanceof FlagEvalHook, equalTo(true)); } + @Test + public void testEvaluationLoggingDisabled() { + Provider provider = + new Provider( + new Options().initTimeout(10, MILLISECONDS).evaluationLogging(false), + mock(Evaluator.class)); + assertThat(provider.getProviderHooks().size(), equalTo(0)); + } + @Test public void testShutdownCleansUpMetrics() throws Exception { Evaluator evaluator = mock(Evaluator.class); From 6b332698e2c3b64c943b63105b9a61ddd356f37e Mon Sep 17 00:00:00 2001 From: typotter Date: Thu, 9 Apr 2026 10:24:11 -0600 Subject: [PATCH 10/15] Fix feature_flag.evaluations metric count always being zero The OTel SDK defaults to DELTA temporality for counters. The Datadog agent converts OTLP delta monotonic sums to rate metrics by dividing by the export interval (10s). Five evaluations in under 1s produce ~0.5, which rounds to zero in the points payload. Force CUMULATIVE temporality on the OtlpHttpMetricExporter so the agent receives an absolute count rather than a rate, making test_ffe_eval_metric_count reliable. --- .../java/datadog/trace/api/openfeature/FlagEvalMetrics.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 6f185511446..84fecf559f3 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -9,6 +9,7 @@ import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import java.io.Closeable; import java.time.Duration; @@ -59,7 +60,10 @@ class FlagEvalMetrics implements Closeable { } OtlpHttpMetricExporter exporter = - OtlpHttpMetricExporter.builder().setEndpoint(endpoint).build(); + OtlpHttpMetricExporter.builder() + .setEndpoint(endpoint) + .setAggregationTemporalitySelector(AggregationTemporalitySelector.alwaysCumulative()) + .build(); PeriodicMetricReader reader = PeriodicMetricReader.builder(exporter).setInterval(EXPORT_INTERVAL).build(); From c3a7955ca45de320cb2b3362da20b83bfe0a9ba0 Mon Sep 17 00:00:00 2001 From: typotter Date: Mon, 13 Apr 2026 14:52:40 -0600 Subject: [PATCH 11/15] test(openfeature): verify cumulative temporality and count accumulation in FlagEvalMetrics --- .../feature-flagging-api/build.gradle.kts | 1 + .../api/openfeature/FlagEvalMetrics.java | 8 +++ .../api/openfeature/FlagEvalMetricsTest.java | 57 ++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index 3999633bd1e..1c368d51b51 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.bundles.junit5) testImplementation(libs.bundles.mockito) testImplementation(libs.moshi) + testImplementation("io.opentelemetry:opentelemetry-sdk-testing:1.47.0") testImplementation("org.awaitility:awaitility:4.3.0") } diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 84fecf559f3..9d66f8d9010 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -101,6 +101,14 @@ class FlagEvalMetrics implements Closeable { this.meterProvider = null; } + /** Package-private constructor for integration testing with an injected SdkMeterProvider. */ + FlagEvalMetrics(SdkMeterProvider sdkMeterProvider) { + meterProvider = sdkMeterProvider; + Meter meter = sdkMeterProvider.meterBuilder(METER_NAME).build(); + counter = + meter.counterBuilder(METRIC_NAME).setUnit(METRIC_UNIT).setDescription(METRIC_DESC).build(); + } + void record( String flagKey, String variant, String reason, ErrorCode errorCode, String allocationKey) { LongCounter c = counter; diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java index 13261f366fe..a8cf20535c8 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java @@ -1,5 +1,6 @@ package datadog.trace.api.openfeature; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -8,6 +9,15 @@ import dev.openfeature.sdk.ErrorCode; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Collection; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -121,7 +131,7 @@ void recordNullReasonBecomesUnknown() { @Test void recordIsNoOpWhenCounterIsNull() { - FlagEvalMetrics metrics = new FlagEvalMetrics(null); + FlagEvalMetrics metrics = new FlagEvalMetrics((LongCounter) null); // Should not throw metrics.record("my-flag", "on", "TARGETING_MATCH", null, null); } @@ -137,6 +147,51 @@ void shutdownClearsCounter() { verifyNoInteractions(counter); } + @Test + void exporterIsConfiguredWithCumulativeTemporalityForCounters() { + // Regression guard: FlagEvalMetrics must explicitly configure alwaysCumulative() so that + // the Datadog agent receives absolute counts rather than delta values that may be converted + // to rates. This test documents and enforces that the exporter uses CUMULATIVE for counters. + try (OtlpHttpMetricExporter exporter = + OtlpHttpMetricExporter.builder() + .setAggregationTemporalitySelector(AggregationTemporalitySelector.alwaysCumulative()) + .build()) { + assertEquals( + AggregationTemporality.CUMULATIVE, + exporter.getAggregationTemporality(InstrumentType.COUNTER), + "alwaysCumulative() selector must produce CUMULATIVE for counters"); + } + } + + @Test + void multipleRecordCallsAccumulateCumulativelyInExportedMetrics() { + // Use InMemoryMetricReader with cumulative temporality (matching what FlagEvalMetrics + // configures on the OTLP exporter) to verify that N record() calls produce a sum of N. + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + try (FlagEvalMetrics metrics = new FlagEvalMetrics(provider)) { + for (int i = 0; i < 5; i++) { + metrics.record("count-flag", "on", "STATIC", null, "default-alloc"); + } + + Collection data = reader.collectAllMetrics(); + MetricData metric = + data.stream() + .filter(m -> m.getName().equals("feature_flag.evaluations")) + .findFirst() + .orElseThrow(() -> new AssertionError("feature_flag.evaluations metric not found")); + + assertEquals( + AggregationTemporality.CUMULATIVE, + metric.getLongSumData().getAggregationTemporality(), + "Exported metric must use CUMULATIVE temporality"); + + LongPointData point = metric.getLongSumData().getPoints().iterator().next(); + assertEquals(5L, point.getValue(), "5 record() calls must produce a cumulative sum of 5"); + } + } + private static void assertAttribute(Attributes attrs, String key, String expected) { String value = attrs.asMap().entrySet().stream() From 95c6fb90b73f237efe7fa6a4378e3da123b9dc0f Mon Sep 17 00:00:00 2001 From: typotter Date: Tue, 14 Apr 2026 01:11:48 -0600 Subject: [PATCH 12/15] Address internal review feedback - Remove exporterIsConfiguredWithCumulativeTemporalityForCounters test (tested OTel SDK, not our code; the integration test is the real regression guard) - Fix Provider catch block comment to reflect that FlagEvalMetrics may not have logged if we reach this point - Include exception in log.error calls for NoClassDefFoundError and general Exception to aid debugging - Reword InMemoryMetricReader comment for precision --- .../api/openfeature/FlagEvalMetrics.java | 5 ++-- .../trace/api/openfeature/Provider.java | 2 +- .../api/openfeature/FlagEvalMetricsTest.java | 24 +++---------------- 3 files changed, 7 insertions(+), 24 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 9d66f8d9010..cb932e3bd79 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -85,11 +85,12 @@ class FlagEvalMetrics implements Closeable { log.error( "Evaluation logging is enabled but OpenTelemetry SDK is not on the classpath. " + "Add opentelemetry-sdk-metrics and opentelemetry-exporter-otlp to your dependencies, " - + "or disable evaluation logging via Provider.Options.evaluationLogging(false)."); + + "or disable evaluation logging via Provider.Options.evaluationLogging(false).", + e); counter = null; meterProvider = null; } catch (Exception e) { - log.error("Failed to initialize flag evaluation metrics: {}", e.getMessage()); + log.error("Failed to initialize flag evaluation metrics", e); counter = null; meterProvider = null; } diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 0d6da65a177..a908e2e576d 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -49,7 +49,7 @@ public Provider(final Options options) { metrics = new FlagEvalMetrics(); hook = new FlagEvalHook(metrics); } catch (NoClassDefFoundError | Exception e) { - // FlagEvalMetrics logs the error — degrade to no-op + // Fallback: FlagEvalMetrics constructor threw unexpectedly — degrade to no-op } } this.flagEvalMetrics = metrics; diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java index a8cf20535c8..cec9b2d0eb7 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/FlagEvalMetricsTest.java @@ -9,13 +9,10 @@ import dev.openfeature.sdk.ErrorCode; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; -import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.MetricData; -import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import java.util.Collection; import org.junit.jupiter.api.Test; @@ -147,26 +144,11 @@ void shutdownClearsCounter() { verifyNoInteractions(counter); } - @Test - void exporterIsConfiguredWithCumulativeTemporalityForCounters() { - // Regression guard: FlagEvalMetrics must explicitly configure alwaysCumulative() so that - // the Datadog agent receives absolute counts rather than delta values that may be converted - // to rates. This test documents and enforces that the exporter uses CUMULATIVE for counters. - try (OtlpHttpMetricExporter exporter = - OtlpHttpMetricExporter.builder() - .setAggregationTemporalitySelector(AggregationTemporalitySelector.alwaysCumulative()) - .build()) { - assertEquals( - AggregationTemporality.CUMULATIVE, - exporter.getAggregationTemporality(InstrumentType.COUNTER), - "alwaysCumulative() selector must produce CUMULATIVE for counters"); - } - } - @Test void multipleRecordCallsAccumulateCumulativelyInExportedMetrics() { - // Use InMemoryMetricReader with cumulative temporality (matching what FlagEvalMetrics - // configures on the OTLP exporter) to verify that N record() calls produce a sum of N. + // InMemoryMetricReader defaults to cumulative temporality. This validates that N record() + // calls produce a cumulative sum of N, matching the alwaysCumulative() selector configured + // on the production OTLP exporter in FlagEvalMetrics. InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); From 51282595fc5f9687fa895ecfd109563484e09f92 Mon Sep 17 00:00:00 2001 From: typotter Date: Tue, 14 Apr 2026 01:24:16 -0600 Subject: [PATCH 13/15] Improve error handling observability - Add debug log to FlagEvalMetrics.record() catch block so metric recording failures are visible in debug logs - Widen Provider catch from NoClassDefFoundError to LinkageError to cover IncompatibleClassChangeError and other classloader issues from incompatible OTel SDK versions - Add slf4j logger to Provider and log at error level when the fallback catch fires --- .../datadog/trace/api/openfeature/FlagEvalMetrics.java | 2 +- .../main/java/datadog/trace/api/openfeature/Provider.java | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index cb932e3bd79..04a2b174fe8 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -133,7 +133,7 @@ void record( c.add(1, builder.build()); } catch (Exception e) { - // Never let metrics recording break flag evaluation + log.debug("Failed to record flag evaluation metric for {}", flagKey, e); } } diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index a908e2e576d..4eb21c887c1 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -19,9 +19,12 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Provider extends EventProvider implements Metadata { + private static final Logger log = LoggerFactory.getLogger(Provider.class); static final String METADATA = "datadog-openfeature-provider"; private static final String EVALUATOR_IMPL = "datadog.trace.api.openfeature.DDEvaluator"; private static final Options DEFAULT_OPTIONS = new Options().initTimeout(30, SECONDS); @@ -48,8 +51,8 @@ public Provider(final Options options) { try { metrics = new FlagEvalMetrics(); hook = new FlagEvalHook(metrics); - } catch (NoClassDefFoundError | Exception e) { - // Fallback: FlagEvalMetrics constructor threw unexpectedly — degrade to no-op + } catch (LinkageError | Exception e) { + log.error("Failed to initialize evaluation metrics — metrics disabled", e); } } this.flagEvalMetrics = metrics; From 4e5cadefa3fccaf03f0fe01b749dbc84adaa23d1 Mon Sep 17 00:00:00 2001 From: typotter Date: Tue, 14 Apr 2026 02:12:08 -0600 Subject: [PATCH 14/15] Use warn level for Provider fallback catch The Provider catch is defense-in-depth for when FlagEvalMetrics class itself can't load (OTel API absent entirely). The detailed error message is logged inside FlagEvalMetrics when it CAN load but SDK init fails. Using error level here caused the openfeature smoke test to fail (it asserts no ERROR entries in application logs). --- .../src/main/java/datadog/trace/api/openfeature/Provider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 4eb21c887c1..54dc0677aa5 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -52,7 +52,9 @@ public Provider(final Options options) { metrics = new FlagEvalMetrics(); hook = new FlagEvalHook(metrics); } catch (LinkageError | Exception e) { - log.error("Failed to initialize evaluation metrics — metrics disabled", e); + // FlagEvalMetrics logs the detailed error when it can load but OTel SDK init fails. + // This outer catch fires when the class itself can't load (OTel API absent entirely). + log.warn("Evaluation metrics unavailable — OTel classes not on classpath", e); } } this.flagEvalMetrics = metrics; From 93af7a8bd4445cb9f96f66e8ba1919db982b02f7 Mon Sep 17 00:00:00 2001 From: typotter Date: Tue, 14 Apr 2026 10:12:12 -0600 Subject: [PATCH 15/15] =?UTF-8?q?Remove=20evaluationLogging=20option=20?= =?UTF-8?q?=E2=80=94=20metrics=20always=20enabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evaluation metrics are always attempted. If the OTel SDK is absent, the provider degrades gracefully with a warning. There is no user- facing toggle to disable metrics — this matches the Go and Python SDKs which also always attempt metrics. --- .../api/openfeature/FlagEvalMetrics.java | 6 ++--- .../trace/api/openfeature/Provider.java | 27 +++++-------------- .../trace/api/openfeature/ProviderTest.java | 9 ------- 3 files changed, 10 insertions(+), 32 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java index 04a2b174fe8..83ca85af9b0 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/FlagEvalMetrics.java @@ -83,9 +83,9 @@ class FlagEvalMetrics implements Closeable { log.debug("Flag evaluation metrics initialized, exporting to {}", endpoint); } catch (NoClassDefFoundError e) { log.error( - "Evaluation logging is enabled but OpenTelemetry SDK is not on the classpath. " - + "Add opentelemetry-sdk-metrics and opentelemetry-exporter-otlp to your dependencies, " - + "or disable evaluation logging via Provider.Options.evaluationLogging(false).", + "OpenTelemetry SDK is not on the classpath — evaluation metrics disabled. " + + "Add opentelemetry-sdk-metrics and opentelemetry-exporter-otlp to your dependencies " + + "to enable flag evaluation metrics.", e); counter = null; meterProvider = null; diff --git a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java index 54dc0677aa5..f16b6e582a8 100644 --- a/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java +++ b/products/feature-flagging/feature-flagging-api/src/main/java/datadog/trace/api/openfeature/Provider.java @@ -47,15 +47,13 @@ public Provider(final Options options) { this.evaluator = evaluator; FlagEvalMetrics metrics = null; FlagEvalHook hook = null; - if (options.isEvaluationLogging()) { - try { - metrics = new FlagEvalMetrics(); - hook = new FlagEvalHook(metrics); - } catch (LinkageError | Exception e) { - // FlagEvalMetrics logs the detailed error when it can load but OTel SDK init fails. - // This outer catch fires when the class itself can't load (OTel API absent entirely). - log.warn("Evaluation metrics unavailable — OTel classes not on classpath", e); - } + try { + metrics = new FlagEvalMetrics(); + hook = new FlagEvalHook(metrics); + } catch (LinkageError | Exception e) { + // FlagEvalMetrics logs the detailed error when it can load but OTel SDK init fails. + // This outer catch fires when the class itself can't load (OTel API absent entirely). + log.warn("Evaluation metrics unavailable — OTel classes not on classpath", e); } this.flagEvalMetrics = metrics; this.flagEvalHook = hook; @@ -166,7 +164,6 @@ public static class Options { private long timeout; private TimeUnit unit; - private boolean evaluationLogging = true; public Options initTimeout(final long timeout, final TimeUnit unit) { this.timeout = timeout; @@ -174,12 +171,6 @@ public Options initTimeout(final long timeout, final TimeUnit unit) { return this; } - /** Enable or disable evaluation logging via OTel metrics. Default: true. */ - public Options evaluationLogging(final boolean enabled) { - this.evaluationLogging = enabled; - return this; - } - public long getTimeout() { return timeout; } @@ -187,9 +178,5 @@ public long getTimeout() { public TimeUnit getUnit() { return unit; } - - public boolean isEvaluationLogging() { - return evaluationLogging; - } } } diff --git a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java index 6ec9bef6f4c..87a80f59e20 100644 --- a/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java +++ b/products/feature-flagging/feature-flagging-api/src/test/java/datadog/trace/api/openfeature/ProviderTest.java @@ -149,15 +149,6 @@ public void testGetProviderHooksReturnsFlagEvalHook() { assertThat(hooks.get(0) instanceof FlagEvalHook, equalTo(true)); } - @Test - public void testEvaluationLoggingDisabled() { - Provider provider = - new Provider( - new Options().initTimeout(10, MILLISECONDS).evaluationLogging(false), - mock(Evaluator.class)); - assertThat(provider.getProviderHooks().size(), equalTo(0)); - } - @Test public void testShutdownCleansUpMetrics() throws Exception { Evaluator evaluator = mock(Evaluator.class);