From 051539dcfa12fec516bca5e35d00d349f7dc59e0 Mon Sep 17 00:00:00 2001 From: Sameeran Kunche Date: Thu, 25 Jun 2026 13:54:26 -0700 Subject: [PATCH] Use agent OTel metrics for OpenFeature evaluations --- .../feature-flagging-api/README.md | 38 +++++--- .../feature-flagging-api/build.gradle.kts | 6 -- .../feature-flagging-api/gradle.lockfile | 21 +---- .../api/openfeature/FlagEvalMetrics.java | 92 ++----------------- .../trace/api/openfeature/Provider.java | 5 +- .../api/openfeature/FlagEvalMetricsTest.java | 40 +------- 6 files changed, 36 insertions(+), 166 deletions(-) diff --git a/products/feature-flagging/feature-flagging-api/README.md b/products/feature-flagging/feature-flagging-api/README.md index 5d1beece1ca..47733020559 100644 --- a/products/feature-flagging/feature-flagging-api/README.md +++ b/products/feature-flagging/feature-flagging-api/README.md @@ -18,22 +18,27 @@ The OpenFeature SDK (`dev.openfeature:sdk`) is included as a transitive dependen ### Evaluation metrics (optional) -To enable evaluation metrics (`feature_flag.evaluations` counter), add the OpenTelemetry SDK dependencies: +To enable evaluation metrics (`feature_flag.evaluations` counter), enable the Datadog Java agent's +OpenTelemetry metrics pipeline: + +```shell +DD_METRICS_OTEL_ENABLED=true +``` + +The provider records metrics through the OpenTelemetry Metrics API. Add `opentelemetry-api` if your +application does not already use the OpenTelemetry API for custom metrics: ```xml io.opentelemetry - opentelemetry-sdk-metrics - 1.47.0 - - - io.opentelemetry - opentelemetry-exporter-otlp + opentelemetry-api 1.47.0 ``` -Any OpenTelemetry API 1.x version is compatible. If these dependencies are absent, the provider operates normally without metrics. +The OpenTelemetry SDK and OTLP exporter are not required on the application classpath. The Datadog +Java agent collects the API metric and exports it through the same OTLP pipeline as other custom OTel +metrics. ## Usage @@ -52,15 +57,20 @@ boolean enabled = client.getBooleanValue("my-feature", false, ## 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. +When `DD_METRICS_OTEL_ENABLED=true` and the OpenTelemetry API is on the classpath, the provider +records a `feature_flag.evaluations` counter. The Datadog Java agent exports it to the Datadog +Agent's OTLP receiver using the configured OpenTelemetry metrics export interval. ### 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` | +Configure the OTLP endpoint and protocol using the standard Datadog Java agent OpenTelemetry metrics +settings. For example, to export metrics over OTLP/gRPC: + +```shell +DD_METRICS_OTEL_ENABLED=true +OTEL_EXPORTER_OTLP_ENDPOINT=http://:4317 +OTEL_EXPORTER_OTLP_PROTOCOL=grpc +``` ### Metric attributes diff --git a/products/feature-flagging/feature-flagging-api/build.gradle.kts b/products/feature-flagging/feature-flagging-api/build.gradle.kts index 1c368d51b51..def6a16da8c 100644 --- a/products/feature-flagging/feature-flagging-api/build.gradle.kts +++ b/products/feature-flagging/feature-flagging-api/build.gradle.kts @@ -44,19 +44,13 @@ 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") 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) - 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/gradle.lockfile b/products/feature-flagging/feature-flagging-api/gradle.lockfile index f3661e92730..31b9288e03a 100644 --- a/products/feature-flagging/feature-flagging-api/gradle.lockfile +++ b/products/feature-flagging/feature-flagging-api/gradle.lockfile @@ -12,10 +12,7 @@ com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClass com.google.code.gson:gson:2.13.2=spotbugs com.google.errorprone:error_prone_annotations:2.41.0=spotbugs com.squareup.moshi:moshi:1.11.0=testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:okhttp:4.12.0=testRuntimeClasspath -com.squareup.okio:okio-jvm:3.6.0=testRuntimeClasspath -com.squareup.okio:okio:1.17.5=testCompileClasspath -com.squareup.okio:okio:3.6.0=testRuntimeClasspath +com.squareup.okio:okio:1.17.5=testCompileClasspath,testRuntimeClasspath com.thoughtworks.qdox:qdox:1.12.1=codenarc commons-io:commons-io:2.20.0=spotbugs de.thetaphi:forbiddenapis:3.10=compileClasspath @@ -23,17 +20,6 @@ dev.openfeature:sdk:1.20.1=compileClasspath,runtimeClasspath,testCompileClasspat io.leangen.geantyref:geantyref:1.3.16=testRuntimeClasspath io.opentelemetry:opentelemetry-api:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath io.opentelemetry:opentelemetry-context:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-common:1.47.0=testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-otlp-common:1.47.0=testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-otlp:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-exporter-sender-okhttp:1.47.0=testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-common:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.47.0=testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-logs:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-metrics:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-testing:1.47.0=testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk-trace:1.47.0=compileClasspath,testCompileClasspath,testRuntimeClasspath -io.opentelemetry:opentelemetry-sdk:1.47.0=testCompileClasspath,testRuntimeClasspath jaxen:jaxen:2.0.0=spotbugs net.bytebuddy:byte-buddy-agent:1.18.10=testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.18.10=testCompileClasspath,testRuntimeClasspath @@ -64,11 +50,6 @@ org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt org.jacoco:org.jacoco.ant:0.8.14=jacocoAnt org.jacoco:org.jacoco.core:0.8.14=jacocoAnt org.jacoco:org.jacoco.report:0.8.14=jacocoAnt -org.jetbrains.kotlin:kotlin-stdlib-common:1.9.10=testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10=testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.10=testRuntimeClasspath -org.jetbrains.kotlin:kotlin-stdlib:1.9.10=testRuntimeClasspath -org.jetbrains:annotations:13.0=testRuntimeClasspath org.junit.jupiter:junit-jupiter-api:5.14.1=testCompileClasspath,testRuntimeClasspath org.junit.jupiter:junit-jupiter-engine:5.14.1=testRuntimeClasspath org.junit.jupiter:junit-jupiter-params:5.14.1=testCompileClasspath,testRuntimeClasspath 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 bcb5898f988..eba3b7ba4b5 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,13 @@ package datadog.trace.api.openfeature; -import datadog.trace.config.inversion.ConfigHelper; 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.AggregationTemporalitySelector; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; -import io.opentelemetry.sdk.resources.Resource; import java.io.Closeable; -import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,17 +19,6 @@ class FlagEvalMetrics implements Closeable { 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"; - // 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"; - // OTel standard env vars for service name - private static final String SERVICE_NAME_ENV = "OTEL_SERVICE_NAME"; - private static final AttributeKey SERVICE_NAME_KEY = - AttributeKey.stringKey("service.name"); private static final AttributeKey ATTR_FLAG_KEY = AttributeKey.stringKey("feature_flag.key"); @@ -48,40 +31,10 @@ class FlagEvalMetrics implements Closeable { AttributeKey.stringKey("feature_flag.result.allocation_key"); 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 Closeable meterProvider; FlagEvalMetrics() { try { - String endpoint = ConfigHelper.env(ENDPOINT_ENV); - if (endpoint == null || endpoint.isEmpty()) { - String base = ConfigHelper.env(ENDPOINT_GENERIC_ENV); - if (base != null && !base.isEmpty()) { - endpoint = base.endsWith("/") ? base + "v1/metrics" : base + "/v1/metrics"; - } else { - endpoint = DEFAULT_ENDPOINT; - } - } - - OtlpHttpMetricExporter exporter = - OtlpHttpMetricExporter.builder() - .setEndpoint(endpoint) - .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) - .build(); - - PeriodicMetricReader reader = - PeriodicMetricReader.builder(exporter).setInterval(EXPORT_INTERVAL).build(); - - // Build resource with service name from OTEL_SERVICE_NAME env var - // Resource.getDefault() only provides unknown_service:java, so we read env vars manually - Resource resource = buildResource(); - - SdkMeterProvider sdkMeterProvider = - SdkMeterProvider.builder().setResource(resource).registerMetricReader(reader).build(); - meterProvider = sdkMeterProvider; - - Meter meter = sdkMeterProvider.meterBuilder(METER_NAME).build(); + Meter meter = GlobalOpenTelemetry.get().getMeterProvider().meterBuilder(METER_NAME).build(); counter = meter .counterBuilder(METRIC_NAME) @@ -89,34 +42,23 @@ class FlagEvalMetrics implements Closeable { .setDescription(METRIC_DESC) .build(); - log.debug("Flag evaluation metrics initialized, exporting to {}", endpoint); + log.debug("Flag evaluation metrics initialized"); } catch (NoClassDefFoundError e) { log.error( - "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.", + "OpenTelemetry API is not on the classpath — evaluation metrics disabled. Add" + + " opentelemetry-api to your dependencies and enable DD_METRICS_OTEL_ENABLED to" + + " export flag evaluation metrics through the Datadog Java agent.", e); counter = null; - meterProvider = null; } catch (Exception e) { log.error("Failed to initialize flag evaluation metrics", e); counter = null; - meterProvider = null; } } /** Package-private constructor for testing with a mock counter. */ FlagEvalMetrics(LongCounter counter) { this.counter = counter; - 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( @@ -153,27 +95,5 @@ public void close() { void shutdown() { counter = null; - Closeable mp = meterProvider; - if (mp != null) { - meterProvider = null; - try { - mp.close(); - } catch (Exception e) { - // Ignore shutdown errors - } - } - } - - /** - * Builds a Resource with the service name from OTEL_SERVICE_NAME environment variable. Falls back - * to Resource.getDefault() if OTEL_SERVICE_NAME is not set. - */ - private static Resource buildResource() { - String serviceName = ConfigHelper.env(SERVICE_NAME_ENV); - if (serviceName != null && !serviceName.isEmpty()) { - return Resource.getDefault() - .merge(Resource.builder().put(SERVICE_NAME_KEY, serviceName).build()); - } - return Resource.getDefault(); } } 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 c492ef49c69..c5e8898fa4a 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 @@ -53,9 +53,8 @@ public Provider(final Options options) { 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 outer catch fires when the metrics helper itself can't load (OTel API absent). + log.warn("Evaluation metrics unavailable — OTel API classes not on classpath", e); } this.flagEvalMetrics = metrics; this.flagEvalHook = hook; 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 9d870b0c03c..214fadc7274 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,6 +1,5 @@ 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; @@ -9,13 +8,6 @@ import dev.openfeature.sdk.ErrorCode; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongCounter; -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; @@ -146,35 +138,9 @@ void shutdownClearsCounter() { } @Test - void multipleRecordCallsAccumulateWithDeltaTemporality() { - // Use delta temporality to match the deltaPreferred() selector configured on the production - // OTLP exporter in FlagEvalMetrics. Delta temporality exports only increments since last - // collection, which is what OTLP receivers expect. - InMemoryMetricReader reader = - InMemoryMetricReader.builder() - .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) - .build(); - 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.DELTA, - metric.getLongSumData().getAggregationTemporality(), - "Exported metric must use DELTA temporality"); - - LongPointData point = metric.getLongSumData().getPoints().iterator().next(); - assertEquals(5L, point.getValue(), "5 record() calls must produce a delta sum of 5"); + void defaultConstructorUsesOpenTelemetryApiOnly() { + try (FlagEvalMetrics metrics = new FlagEvalMetrics()) { + metrics.record("count-flag", "on", "STATIC", null, "default-alloc"); } }