Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
63 changes: 63 additions & 0 deletions tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public FeatureFlagsEvaluator(ReportExposureDelegate? onExposureEvent, ServerConf
_onExposureEvent = onExposureEvent;
_config = config;
_spanEnrichmentEnabled = spanEnrichmentEnabled;

// Eagerly validate the config: pre-parse SemVer comparands and
// identify flags with invalid configuration before any evaluation.
_config?.Validate();
if (_config is null)
{
Log.Debug("Creating Evaluator without config");
Expand Down Expand Up @@ -83,6 +87,23 @@ public Evaluation Evaluate(string flagKey, ValueType resultType, object? default
});
}

// Check if the flag was marked invalid during config validation
// (e.g., invalid SemVer comparand). Return PARSE_ERROR before
// attempting evaluation.
if (config.InvalidFlags is not null && config.InvalidFlags.TryGetValue(flagKey, out var validationError))
{
return new Evaluation(
flagKey,
defaultValue,
EvaluationReason.Error,
error: "PARSE_ERROR",
metadata: new Dictionary<string, string>
{
["errorCode"] = "PARSE_ERROR",
["message"] = validationError
});
}

if (flag.Enabled != true)
{
return new Evaluation(
Expand Down Expand Up @@ -302,6 +323,13 @@ private static bool EvaluateCondition(ConditionConfiguration condition, Evaluati
return CompareNumber(attributeValue, condition.Value, (a, b) => a <= b);
case ConditionOperator.LT:
return CompareNumber(attributeValue, condition.Value, (a, b) => a < b);
case ConditionOperator.SEMVER_EQ:
case ConditionOperator.SEMVER_NEQ:
case ConditionOperator.SEMVER_LT:
case ConditionOperator.SEMVER_LTE:
case ConditionOperator.SEMVER_GT:
case ConditionOperator.SEMVER_GTE:
return EvaluateSemverCondition(attributeValue, condition, condition.Operator.Value);
default:
throw new FormatException($"Unknown condition operator {condition.Operator.ToString()}");
}
Expand Down Expand Up @@ -371,6 +399,41 @@ private static bool CompareNumber(object attributeValue, object? conditionValue,
return comparator(a, b);
}

private static bool EvaluateSemverCondition(object attributeValue, ConditionConfiguration condition, ConditionOperator operatorValue)
{
if (attributeValue is not string attribute)
{
return false;
}

// Comparand was pre-parsed during config validation. If null, the
// flag should have been marked invalid and this method should not
// be reached. Return false as a safety net.
var comparand = condition.SemverComparand;
if (comparand is null)
{
return false;
}

if (!SemVer.TryParse(attribute, out var parsedAttribute))
{
return false;
}

var ordering = SemVer.Compare(parsedAttribute, comparand.Value);

return operatorValue switch
{
ConditionOperator.SEMVER_EQ => ordering == 0,
ConditionOperator.SEMVER_NEQ => ordering != 0,
ConditionOperator.SEMVER_LT => ordering < 0,
ConditionOperator.SEMVER_LTE => ordering <= 0,
ConditionOperator.SEMVER_GT => ordering > 0,
ConditionOperator.SEMVER_GTE => ordering >= 0,
_ => false,
};
}

private static bool MatchesShard(Shard shard, string? targetingKey)
{
if (shard.Ranges is null)
Expand Down
39 changes: 39 additions & 0 deletions tracer/src/Datadog.Trace/FeatureFlags/ParsedSemVer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// <copyright file="ParsedSemVer.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;

namespace Datadog.Trace.FeatureFlags;

/// <summary>
/// The language-neutral representation of the Rust/Eppo SemVer subset used by FFE.
/// </summary>
internal readonly struct ParsedSemVer : IEquatable<ParsedSemVer>
{
public ParsedSemVer(ulong major, ulong minor, ulong patch, string prerelease)
{
Major = major;
Minor = minor;
Patch = patch;
Prerelease = prerelease;
}

public ulong Major { get; }

public ulong Minor { get; }

public ulong Patch { get; }

public string Prerelease { get; }

public bool Equals(ParsedSemVer other)
=> Major == other.Major && Minor == other.Minor && Patch == other.Patch && Prerelease == other.Prerelease;

public override bool Equals(object? obj) => obj is ParsedSemVer other && Equals(other);

public override int GetHashCode() => HashCode.Combine(Major, Minor, Patch, Prerelease);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,44 @@ namespace Datadog.Trace.FeatureFlags.Rcm.Model;
internal sealed class ConditionConfiguration
{
private Regex? _regex;
private ParsedSemVer? _semverComparand;

public ConditionOperator? Operator { get; set; }

public string? Attribute { get; set; }

public object? Value { get; set; }

/// <summary>
/// Gets the validated, parsed SemVer condition value for SEMVER_* operators.
/// Populated eagerly during config validation by <see cref="TryPreparseSemverComparand"/>.
/// Null if the comparand is invalid or the operator is not a SEMVER_* operator.
/// </summary>
internal ParsedSemVer? SemverComparand => _semverComparand;

/// <summary>
/// Eagerly parses the SemVer comparand for SEMVER_* operators.
/// Called during config validation, not during evaluation.
/// Returns true if the comparand is valid or the operator is not a SEMVER_* operator.
/// Returns false if the operator is a SEMVER_* operator and the comparand is invalid.
/// </summary>
internal bool TryPreparseSemverComparand()
{
if (Operator is ConditionOperator.SEMVER_EQ or ConditionOperator.SEMVER_NEQ or ConditionOperator.SEMVER_LT
or ConditionOperator.SEMVER_LTE or ConditionOperator.SEMVER_GT or ConditionOperator.SEMVER_GTE)
{
if (Value is string comparand && SemVer.TryParse(comparand, out var parsed))
{
_semverComparand = parsed;
return true;
}

return false;
}

return true;
}

internal bool MatchesRegex(object attributeValue)
{
if (_regex == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ internal enum ConditionOperator
NOT_MATCHES,
ONE_OF,
NOT_ONE_OF,
IS_NULL
IS_NULL,
SEMVER_EQ,
SEMVER_NEQ,
SEMVER_LT,
SEMVER_LTE,
SEMVER_GT,
SEMVER_GTE
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Datadog.Trace.FeatureFlags.Rcm.Model;

internal sealed class ServerConfiguration
{
private bool _validated;

public string? CreatedAt { get; set; }

public string? Format { get; set; }
Expand All @@ -20,8 +22,87 @@ internal sealed class ServerConfiguration

public Dictionary<string, Flag>? Flags { get; set; }

/// <summary>
/// Gets or sets the collection of flags that failed validation (e.g., invalid SemVer comparands).
/// Maps flag key to an error message describing the validation failure.
/// Populated by <see cref="Validate"/>.
/// </summary>
internal Dictionary<string, string>? InvalidFlags { get; set; }

/// <summary>
/// Validates all flags by pre-parsing SemVer comparands and identifying
/// flags with invalid configuration. This is called eagerly during config
/// loading (before evaluation) so that invalid flags can be detected and
/// reported without waiting for an evaluation to trigger the error.
/// Idempotent: safe to call multiple times.
/// </summary>
internal void Validate()
{
if (_validated)
{
return;
}

_validated = true;

if (Flags is null)
{
return;
}

foreach (var pair in Flags)
{
var flagKey = pair.Key;
var flag = pair.Value;
if (flag?.Allocations is null)
{
continue;
}

foreach (var allocation in flag.Allocations)
{
if (allocation.Rules is null)
{
continue;
}

foreach (var rule in allocation.Rules)
{
if (rule.Conditions is null)
{
continue;
}

foreach (var condition in rule.Conditions)
{
if (!condition.TryPreparseSemverComparand())
{
InvalidFlags ??= new Dictionary<string, string>();
InvalidFlags[flagKey] = $"Invalid semantic version comparand for flag \"{flagKey}\"";
break;
}
}

if (InvalidFlags is not null && InvalidFlags.ContainsKey(flagKey))
{
break;
}
}

if (InvalidFlags is not null && InvalidFlags.ContainsKey(flagKey))
{
break;
}
}
}
}

internal void Merge(ServerConfiguration other)
{
// Merging changes the flag set, so the merged config needs re-validation.
_validated = false;
InvalidFlags = null;

if (other.CreatedAt is not null)
{
CreatedAt = other.CreatedAt;
Expand Down
Loading
Loading