Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.datadog.openfeature;

import static com.datadog.openfeature.internal.evaluation.MetadataKeys.ALLOCATION_KEY;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.DO_LOG;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.EVALUATION_FEATURE_PREFIX;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.EXPOSURE_FEATURE_PREFIX;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.HAS_EVALUATION_FEATURES;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.OBSERVE_FULL_EVALUATION_DATA;
import static com.datadog.openfeature.internal.evaluation.MetadataKeys.SPLIT_SERIAL_ID;

import com.datadog.openfeature.internal.FeatureFlagsRuntime;
import com.datadog.openfeature.internal.bridge.v1.AgentBridge;
import com.datadog.openfeature.internal.config.Settings;
Expand All @@ -8,10 +16,13 @@
import com.datadog.openfeature.internal.exposure.ExposureDeduplicationCache;
import com.datadog.openfeature.internal.exposure.ExposureEvent;
import com.datadog.openfeature.internal.exposure.Subject;
import com.datadog.openfeature.internal.flagevaluation.FlagEvalEvent;
import com.datadog.openfeature.internal.flagevaluation.FlagEvaluationWriter;
import com.datadog.openfeature.internal.ufc.Allocation;
import com.datadog.openfeature.internal.ufc.ServerConfiguration;
import com.datadog.openfeature.internal.ufc.ValueType;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.ImmutableStructure;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Structure;
Expand Down Expand Up @@ -81,6 +92,13 @@ class DDEvaluator implements Evaluator, Consumer<ServerConfiguration> {
private final Settings settings;
private final FlagEvaluator core;
private final ConfigurationStore configuration = new ConfigurationStore();
// Dependency telemetry belongs to one OpenFeature hook lifecycle. The exposure hook opens a
// scope before provider evaluation and closes it in finallyAfter. Evaluation hooks inspect the
// staged records before that close, so dependency and root telemetry share the same opt-out and
// application-hook boundary. Direct provider calls have no scope and emit no telemetry. A stack
// keeps nested evaluations on the same thread isolated.
private final ThreadLocal<Deque<DependencyTelemetryScope>> dependencyTelemetryScopes =
new ThreadLocal<>();
private volatile FeatureFlagsRuntime runtime;

DDEvaluator(final Runnable configCallback, final AgentBridge bridge, final Settings settings) {
Expand Down Expand Up @@ -125,6 +143,7 @@ public boolean hasConfiguration() {

@Override
public synchronized void shutdown() {
dependencyTelemetryScopes.remove();
final FeatureFlagsRuntime released = this.runtime;
this.runtime = null;
if (released != null) {
Expand Down Expand Up @@ -157,7 +176,11 @@ public <T> ProviderEvaluation<T> evaluate(
final String key,
final T defaultValue,
final EvaluationContext context) {
return core.evaluate(configuration.get(), target, key, defaultValue, context);
final FlagEvaluator.EvaluationResult<T> evaluation =
core.evaluateWithTrace(configuration.get(), target, key, defaultValue, context);
final ProviderEvaluation<T> result = evaluation.details;
stageDependencyTelemetry(key, context, evaluation);
return result;
}

static boolean isAllocationActive(final Allocation allocation, final Instant now) {
Expand Down Expand Up @@ -242,6 +265,247 @@ void recordExposure(final ExposureEvent event) {
}
}

void publishDependencyEvaluation(
final FlagEvaluator.DependencyEvaluation dependency,
final EvaluationContext context,
final long evaluationTimestampMs,
final FlagEvaluationWriter writer) {
final ImmutableMetadata metadata = dependency.metadata;
final Map<String, Object> values =
metadata == null ? Collections.<String, Object>emptyMap() : metadata.asUnmodifiableMap();
final Object allocationValue = values.get(ALLOCATION_KEY);
final String allocationKey =
allocationValue instanceof String ? (String) allocationValue : null;
if (writer == null || !writer.hasCapacityForEnqueue()) {
if (writer != null) {
writer.countPreQueueOverflow();
}
return;
}
final boolean observeFullEvaluationData =
Boolean.TRUE.equals(values.get(OBSERVE_FULL_EVALUATION_DATA));
String errorMessage = observeFullEvaluationData ? dependency.errorMessage : null;
if ((errorMessage == null || errorMessage.isEmpty()) && dependency.errorCode != null) {
errorMessage = dependency.errorCode.name();
}
final Map<String, Object> attrs;
if (observeFullEvaluationData) {
final CopyResult copy = copyPrunedContext(context);
if (copy.truncatedReason != null) {
writer.countContextTruncated(copy.truncatedReason);
}
attrs = copy.attrs;
} else {
attrs = Collections.emptyMap();
}
final Map<String, Object> features =
Boolean.TRUE.equals(values.get(HAS_EVALUATION_FEATURES))
? SplitFeatureMetadata.featuresWithPrefix(metadata, EVALUATION_FEATURE_PREFIX)
: Collections.<String, Object>emptyMap();
writer.enqueue(
new FlagEvalEvent(
dependency.flagKey,
dependency.variant,
allocationKey,
context == null ? null : context.getTargetingKey(),
errorMessage,
evaluationTimestampMs,
observeFullEvaluationData,
attrs,
features));
}

void publishDependencyEvaluations(
final DependencyEvaluationTrace trace,
final EvaluationContext context,
final FlagEvaluationWriter writer) {
for (final FlagEvaluator.DependencyEvaluation dependency : trace.evaluations) {
try {
publishDependencyEvaluation(dependency, context, trace.evaluationTimestampMs, writer);
} catch (final RuntimeException | LinkageError ignored) {
// One telemetry record must not suppress the root or later dependency records.
}
}
}

void recordDependencyMetrics(
final DependencyEvaluationTrace trace, final FlagEvalMetrics metrics) {
if (metrics == null) {
return;
}
for (final FlagEvaluator.DependencyEvaluation dependency : trace.evaluations) {
try {
final ImmutableMetadata metadata = dependency.metadata;
final Map<String, Object> values =
metadata == null
? Collections.<String, Object>emptyMap()
: metadata.asUnmodifiableMap();
final Object allocationValue = values.get(ALLOCATION_KEY);
metrics.record(
dependency.flagKey,
dependency.variant,
dependency.reason,
dependency.errorCode,
allocationValue instanceof String ? (String) allocationValue : null);
} catch (final RuntimeException | LinkageError ignored) {
// Metrics are best effort and must not affect evaluation or the root metric.
}
}
}

void beginDependencyExposureScope(
final String rootFlagKey, final EvaluationContext evaluationContext) {
Deque<DependencyTelemetryScope> scopes = dependencyTelemetryScopes.get();
if (scopes == null) {
scopes = new LinkedList<>();
dependencyTelemetryScopes.set(scopes);
}
scopes.push(new DependencyTelemetryScope(rootFlagKey, evaluationContext));
}

List<FlagEvaluator.DependencyExposure> endDependencyExposureScope() {
final Deque<DependencyTelemetryScope> scopes = dependencyTelemetryScopes.get();
if (scopes == null || scopes.isEmpty()) {
return Collections.emptyList();
}
final List<FlagEvaluator.DependencyExposure> exposures = scopes.pop().exposures;
if (scopes.isEmpty()) {
dependencyTelemetryScopes.remove();
}
return exposures;
}

DependencyEvaluationTrace dependencyEvaluationTrace(
final String rootFlagKey, final EvaluationContext evaluationContext) {
final Deque<DependencyTelemetryScope> scopes = dependencyTelemetryScopes.get();
if (scopes == null || scopes.isEmpty()) {
return DependencyEvaluationTrace.EMPTY;
}
final DependencyTelemetryScope scope = scopes.peek();
return scope.sealed && scope.matches(rootFlagKey, evaluationContext)
? scope.evaluationTrace
: DependencyEvaluationTrace.EMPTY;
}

private void stageDependencyTelemetry(
final String rootFlagKey,
final EvaluationContext evaluationContext,
final FlagEvaluator.EvaluationResult<?> evaluation) {
final Deque<DependencyTelemetryScope> scopes = dependencyTelemetryScopes.get();
if (scopes == null || scopes.isEmpty()) {
return;
}
final DependencyTelemetryScope scope = scopes.peek();
if (scope.sealed || !scope.matches(rootFlagKey, evaluationContext)) {
return;
}
// The first matching provider evaluation owns this hook lifecycle, even when it has no
// eligible candidates. This prevents a later direct or reentrant evaluation from filling the
// scope before finallyAfter consumes it.
scope.sealed = true;
scope.evaluationTrace =
new DependencyEvaluationTrace(
evaluation.dependencyEvaluations, evaluation.evaluationTimestampMs);
final List<FlagEvaluator.DependencyExposure> candidates =
evaluation.details.getErrorCode() == null
? evaluation.dependencyExposures
: Collections.<FlagEvaluator.DependencyExposure>emptyList();
if (candidates == null || candidates.isEmpty()) {
return;
}
List<FlagEvaluator.DependencyExposure> eligible = null;
for (final FlagEvaluator.DependencyExposure candidate : candidates) {
final ImmutableMetadata metadata = candidate.metadata;
final Map<String, Object> values =
metadata == null ? Collections.<String, Object>emptyMap() : metadata.asUnmodifiableMap();
if (!Boolean.TRUE.equals(values.get(DO_LOG))
|| !(values.get(ALLOCATION_KEY) instanceof String)
|| candidate.variant == null) {
continue;
}
if (eligible == null) {
eligible = new ArrayList<>();
}
eligible.add(candidate);
}
if (eligible != null) {
scope.exposures = Collections.unmodifiableList(eligible);
}
}

void recordDependencyExposures(
final List<FlagEvaluator.DependencyExposure> exposures, final EvaluationContext context) {
final ExposureDeduplicationCache cache = exposureCache();
if (cache == null || context == null) {
return;
}
for (final FlagEvaluator.DependencyExposure exposure : exposures) {
final ImmutableMetadata metadata = exposure.metadata;
final Map<String, Object> values =
metadata == null ? Collections.<String, Object>emptyMap() : metadata.asUnmodifiableMap();
if (!Boolean.TRUE.equals(values.get(DO_LOG))) {
continue;
}
final Object allocationValue = values.get(ALLOCATION_KEY);
final String allocationKey =
allocationValue instanceof String ? (String) allocationValue : null;
final String variantKey = exposure.variant;
if (allocationKey == null || variantKey == null) {
continue;
}
final Object serialValue = values.get(SPLIT_SERIAL_ID);
final Integer serialId = serialValue instanceof Integer ? (Integer) serialValue : null;
if (cache.recordIfChanged(
exposure.flagKey, context.getTargetingKey(), allocationKey, variantKey, serialId)
&& isExposureDeliveryEnabled()) {
recordExposure(
exposureEvent(
exposure.flagKey,
allocationKey,
variantKey,
context,
serialId,
SplitFeatureMetadata.featuresWithPrefix(metadata, EXPOSURE_FEATURE_PREFIX)));
}
}
}

static final class DependencyEvaluationTrace {
private static final DependencyEvaluationTrace EMPTY =
new DependencyEvaluationTrace(Collections.emptyList(), 0);

final List<FlagEvaluator.DependencyEvaluation> evaluations;
final long evaluationTimestampMs;

private DependencyEvaluationTrace(
final List<FlagEvaluator.DependencyEvaluation> evaluations,
final long evaluationTimestampMs) {
this.evaluations = evaluations;
this.evaluationTimestampMs = evaluationTimestampMs;
}
}

private static final class DependencyTelemetryScope {
private final String rootFlagKey;
private final EvaluationContext evaluationContext;
private DependencyEvaluationTrace evaluationTrace = DependencyEvaluationTrace.EMPTY;
private List<FlagEvaluator.DependencyExposure> exposures = Collections.emptyList();
private boolean sealed;

private DependencyTelemetryScope(
final String rootFlagKey, final EvaluationContext evaluationContext) {
this.rootFlagKey = rootFlagKey;
this.evaluationContext = evaluationContext;
}

private boolean matches(
final String evaluatedFlagKey, final EvaluationContext evaluatedContext) {
return rootFlagKey != null
&& rootFlagKey.equals(evaluatedFlagKey)
&& evaluationContext == evaluatedContext;
}
}

static AbstractMap<String, Object> flattenContext(final EvaluationContext context) {
return flattenValues(snapshotValues(context));
}
Expand Down
Loading
Loading