diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 2661bf88..d1037452 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -373,56 +373,13 @@ private async ValueTask EvaluateFeature(string featur Activity.Current != null && Activity.Current.IsAllDataRequested) { - AddEvaluationActivityEvent(evaluationEvent); + FeatureEvaluationTelemetry.Publish(evaluationEvent, Logger); } } return evaluationEvent; } - private void AddEvaluationActivityEvent(EvaluationEvent evaluationEvent) - { - Debug.Assert(evaluationEvent != null); - Debug.Assert(evaluationEvent.FeatureDefinition != null); - - var tags = new ActivityTagsCollection() - { - { "FeatureName", evaluationEvent.FeatureDefinition.Name }, - { "Enabled", evaluationEvent.Enabled }, - { "VariantAssignmentReason", evaluationEvent.VariantAssignmentReason }, - { "Version", ActivitySource.Version } - }; - - if (!string.IsNullOrEmpty(evaluationEvent.TargetingContext?.UserId)) - { - tags["TargetingId"] = evaluationEvent.TargetingContext.UserId; - } - - if (!string.IsNullOrEmpty(evaluationEvent.Variant?.Name)) - { - tags["Variant"] = evaluationEvent.Variant.Name; - } - - if (evaluationEvent.FeatureDefinition.Telemetry.Metadata != null) - { - foreach (KeyValuePair kvp in evaluationEvent.FeatureDefinition.Telemetry.Metadata) - { - if (tags.ContainsKey(kvp.Key)) - { - Logger?.LogWarning($"{kvp.Key} from telemetry metadata will be ignored, as it would override an existing key."); - - continue; - } - - tags[kvp.Key] = kvp.Value; - } - } - - var activityEvent = new ActivityEvent("FeatureFlag", DateTimeOffset.UtcNow, tags); - - Activity.Current.AddEvent(activityEvent); - } - private async ValueTask IsEnabledAsync(FeatureDefinition featureDefinition, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { Debug.Assert(featureDefinition != null); diff --git a/src/Microsoft.FeatureManagement/Telemetry/FeatureEvaluationTelemetry.cs b/src/Microsoft.FeatureManagement/Telemetry/FeatureEvaluationTelemetry.cs new file mode 100644 index 00000000..26952ca5 --- /dev/null +++ b/src/Microsoft.FeatureManagement/Telemetry/FeatureEvaluationTelemetry.cs @@ -0,0 +1,98 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.FeatureManagement.Telemetry +{ + internal static class FeatureEvaluationTelemetry + { + private static readonly string EvaluationEventVersion = "1.0.0"; + + /// + /// Handles an evaluation event by adding it as an activity event to the current Activity. + /// + /// The to publish as an + /// Optional logger to log warnings to + public static void Publish(EvaluationEvent evaluationEvent, ILogger logger) + { + if (Activity.Current == null) + { + throw new InvalidOperationException("An Activity must be created before calling this method."); + } + + if (evaluationEvent == null) + { + throw new ArgumentNullException(nameof(evaluationEvent)); + } + + if (evaluationEvent.FeatureDefinition == null) + { + throw new ArgumentNullException(nameof(evaluationEvent.FeatureDefinition)); + } + + var tags = new ActivityTagsCollection() + { + { "FeatureName", evaluationEvent.FeatureDefinition.Name }, + { "Enabled", evaluationEvent.Enabled }, + { "VariantAssignmentReason", evaluationEvent.VariantAssignmentReason }, + { "Version", EvaluationEventVersion } + }; + + if (!string.IsNullOrEmpty(evaluationEvent.TargetingContext?.UserId)) + { + tags["TargetingId"] = evaluationEvent.TargetingContext.UserId; + } + + if (!string.IsNullOrEmpty(evaluationEvent.Variant?.Name)) + { + tags["Variant"] = evaluationEvent.Variant.Name; + } + + if (evaluationEvent.FeatureDefinition.Telemetry.Metadata != null) + { + foreach (KeyValuePair kvp in evaluationEvent.FeatureDefinition.Telemetry.Metadata) + { + if (tags.ContainsKey(kvp.Key)) + { + logger?.LogWarning($"{kvp.Key} from telemetry metadata will be ignored, as it would override an existing key."); + + continue; + } + + tags[kvp.Key] = kvp.Value; + } + } + + // VariantAssignmentPercentage + if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.DefaultWhenEnabled) + { + // If the variant was assigned due to DefaultWhenEnabled, the percentage reflects the unallocated percentiles + double allocatedPercentage = evaluationEvent.FeatureDefinition.Allocation?.Percentile?.Sum(p => p.To - p.From) ?? 0; + + tags["VariantAssignmentPercentage"] = 100 - allocatedPercentage; + } + else if (evaluationEvent.VariantAssignmentReason == VariantAssignmentReason.Percentile) + { + // If the variant was assigned due to Percentile, the percentage is the sum of the allocated percentiles for the given variant + if (evaluationEvent.FeatureDefinition.Allocation?.Percentile != null) + { + tags["VariantAssignmentPercentage"] = evaluationEvent.FeatureDefinition.Allocation.Percentile + .Where(p => p.Variant == evaluationEvent.Variant?.Name) + .Sum(p => p.To - p.From); + } + } + + // DefaultWhenEnabled + if (evaluationEvent.FeatureDefinition.Allocation?.DefaultWhenEnabled != null) + { + tags["DefaultWhenEnabled"] = evaluationEvent.FeatureDefinition.Allocation.DefaultWhenEnabled; + } + + var activityEvent = new ActivityEvent("FeatureFlag", DateTimeOffset.UtcNow, tags); + + Activity.Current.AddEvent(activityEvent); + } + } +} diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index d12fe51c..989a1168 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -1756,6 +1756,9 @@ public async Task TelemetryPublishing() string label = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Label").Value?.ToString(); string firstTag = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Tags.Tag1").Value?.ToString(); + string variantAssignmentPercentage = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "VariantAssignmentPercentage").Value?.ToString(); + string defaultWhenEnabled = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "DefaultWhenEnabled").Value?.ToString(); + // Test telemetry cases switch (featureName) { @@ -1783,6 +1786,8 @@ public async Task TelemetryPublishing() Assert.Equal("True", enabled); Assert.Equal("Medium", variantName); Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + Assert.Equal("100", variantAssignmentPercentage); + Assert.Equal("Medium", defaultWhenEnabled); break; case Features.VariantFeatureDefaultDisabled: @@ -1791,6 +1796,8 @@ public async Task TelemetryPublishing() Assert.Equal("False", enabled); Assert.Equal("Small", variantName); Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeaturePercentileOn: @@ -1813,6 +1820,8 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Null(variantName); Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeatureUser: @@ -1820,6 +1829,8 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Equal("Small", variantName); Assert.Equal(VariantAssignmentReason.User.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeatureGroup: @@ -1827,6 +1838,8 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Equal("Small", variantName); Assert.Equal(VariantAssignmentReason.Group.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeatureNoVariants: @@ -1834,6 +1847,8 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Null(variantName); Assert.Equal(VariantAssignmentReason.None.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeatureNoAllocation: @@ -1841,6 +1856,8 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Null(variantName); Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + Assert.Equal("100", variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); break; case Features.VariantFeatureAlwaysOffNoAllocation: @@ -1848,6 +1865,17 @@ public async Task TelemetryPublishing() currentTest = 0; Assert.Null(variantName); Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + Assert.Null(variantAssignmentPercentage); + Assert.Null(defaultWhenEnabled); + break; + + case Features.VariantFeatureIncorrectDefaultWhenEnabled: + Assert.Equal(13, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + Assert.Equal("100", variantAssignmentPercentage); + Assert.Equal("Foo", defaultWhenEnabled); break; default: @@ -1912,6 +1940,10 @@ public async Task TelemetryPublishing() await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, cancellationToken); Assert.Equal(0, currentTest); + currentTest = 13; + await featureManager.GetVariantAsync(Features.VariantFeatureIncorrectDefaultWhenEnabled, cancellationToken); + Assert.Equal(0, currentTest); + // Test a feature with telemetry disabled- should throw if the listener hits it bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, cancellationToken); diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index e0746fef..99baf728 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -23,6 +23,7 @@ static class Features public const string VariantFeatureGroup = "VariantFeatureGroup"; public const string VariantFeatureNoVariants = "VariantFeatureNoVariants"; public const string VariantFeatureNoAllocation = "VariantFeatureNoAllocation"; + public const string VariantFeatureIncorrectDefaultWhenEnabled = "VariantFeatureIncorrectDefaultWhenEnabled"; public const string VariantFeatureAlwaysOffNoAllocation = "VariantFeatureAlwaysOffNoAllocation"; public const string VariantFeatureInvalidStatusOverride = "VariantFeatureInvalidStatusOverride"; public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo"; diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index b1ad1e72..e192a5ff 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -209,7 +209,7 @@ "to": 50 } ], - "seed": 1234 + "seed": "1234" }, "telemetry": { "enabled": true @@ -231,7 +231,7 @@ "to": 50 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true @@ -253,7 +253,7 @@ "to": 100 } ], - "seed": 12345 + "seed": "12345" }, "telemetry": { "enabled": true @@ -383,6 +383,22 @@ "enabled": true } }, + { + "id": "VariantFeatureIncorrectDefaultWhenEnabled", + "enabled": true, + "variants": [ + { + "name": "Small", + "configuration_value": "300px" + } + ], + "allocation": { + "default_when_enabled": "Foo" + }, + "telemetry": { + "enabled": true + } + }, { "id": "VariantFeatureAlwaysOffNoAllocation", "enabled": false,