diff --git a/examples/EvaluationDataToApplicationInsights/Program.cs b/examples/EvaluationDataToApplicationInsights/Program.cs index 84253a85..9fefb583 100644 --- a/examples/EvaluationDataToApplicationInsights/Program.cs +++ b/examples/EvaluationDataToApplicationInsights/Program.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -using Microsoft.FeatureManagement.Telemetry; using Microsoft.FeatureManagement; using EvaluationDataToApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; @@ -31,7 +30,7 @@ // Wire up evaluation event emission builder.Services.AddFeatureManagement() .WithTargeting() - .AddTelemetryPublisher(); + .AddApplicationInsightsTelemetryPublisher(); // // Default code from .NET template below diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsEventPublisher.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsEventPublisher.cs new file mode 100644 index 00000000..96c7bdb6 --- /dev/null +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsEventPublisher.cs @@ -0,0 +1,60 @@ +using Microsoft.ApplicationInsights; +using System.Diagnostics; + +namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights +{ + /// + /// Listens to events from feature management and sends them to Application Insights. + /// + internal sealed class ApplicationInsightsEventPublisher : IDisposable + { + private readonly TelemetryClient _telemetryClient; + private readonly ActivityListener _activityListener; + + /// + /// Initializes a new instance of the class. + /// + /// The Application Insights telemetry client. + public ApplicationInsightsEventPublisher(TelemetryClient telemetryClient) + { + _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); + + _activityListener = new ActivityListener + { + ShouldListenTo = (activitySource) => activitySource.Name == "Microsoft.FeatureManagement", + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + ActivityStopped = (activity) => + { + ActivityEvent? evaluationEvent = activity.Events.FirstOrDefault((activityEvent) => activityEvent.Name == "feature_flag"); + + if (evaluationEvent.HasValue && evaluationEvent.Value.Tags.Any()) + { + HandleFeatureFlagEvent(evaluationEvent.Value); + } + } + }; + + ActivitySource.AddActivityListener(_activityListener); + } + + /// + /// Disposes the resources used by the . + /// + public void Dispose() + { + _activityListener.Dispose(); + } + + private void HandleFeatureFlagEvent(ActivityEvent activityEvent) + { + var properties = new Dictionary(); + + foreach (var tag in activityEvent.Tags) + { + properties[tag.Key] = tag.Value?.ToString(); + } + + _telemetryClient.TrackEvent("FeatureEvaluation", properties); + } + } +} diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsHostedService.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsHostedService.cs new file mode 100644 index 00000000..8edc8be1 --- /dev/null +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsHostedService.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.FeatureManagement.Telemetry.ApplicationInsights +{ + /// + /// A hosted service used to construct and dispose the + /// + internal sealed class ApplicationInsightsHostedService : IHostedService + { + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The to get the publisher from. + public ApplicationInsightsHostedService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + } + + /// + /// Uses the service provider to construct a which will start listening for activities. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + public Task StartAsync(CancellationToken cancellationToken) + { + _serviceProvider.GetService(); + + return Task.CompletedTask; + } + + /// + /// Stops this hosted service. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs deleted file mode 100644 index 20b05299..00000000 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/ApplicationInsightsTelemetryPublisher.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.ApplicationInsights; -using System.Diagnostics; - -namespace Microsoft.FeatureManagement.Telemetry -{ - /// - /// Used to publish data from evaluation events to Application Insights - /// - public class ApplicationInsightsTelemetryPublisher : ITelemetryPublisher - { - private const string _eventName = "FeatureEvaluation"; - private readonly TelemetryClient _telemetryClient; - - /// - /// Creates an instance of the Application Insights telemetry publisher - /// - /// The underlying telemetry client that will be used to send data to Application Insights - /// Thrown if the provided telemetry client is null. - public ApplicationInsightsTelemetryPublisher(TelemetryClient telemetryClient) - { - _telemetryClient = telemetryClient ?? throw new ArgumentNullException(nameof(telemetryClient)); - } - - /// - /// Publishes a custom event to Application Insights using data from the given evaluation event. - /// - /// The event to publish. - /// A cancellation token. - /// Returns a ValueTask that represents the asynchronous operation - public ValueTask PublishEvent(EvaluationEvent evaluationEvent, CancellationToken cancellationToken) - { - ValidateEvent(evaluationEvent); - - FeatureDefinition featureDefinition = evaluationEvent.FeatureDefinition; - - var properties = new Dictionary() - { - { "FeatureName", featureDefinition.Name }, - { "Enabled", evaluationEvent.Enabled.ToString() } - }; - - if (evaluationEvent.TargetingContext != null) - { - properties["TargetingId"] = evaluationEvent.TargetingContext.UserId; - } - - if (evaluationEvent.VariantAssignmentReason != VariantAssignmentReason.None) - { - properties["Variant"] = evaluationEvent.Variant?.Name; - - properties["VariantAssignmentReason"] = ToString(evaluationEvent.VariantAssignmentReason); - } - - if (featureDefinition.Telemetry.Metadata != null) - { - foreach (KeyValuePair kvp in featureDefinition.Telemetry.Metadata) - { - properties[kvp.Key] = kvp.Value; - } - } - - _telemetryClient.TrackEvent(_eventName, properties); - - return new ValueTask(); - } - - private void ValidateEvent(EvaluationEvent evaluationEvent) - { - if (evaluationEvent == null) - { - throw new ArgumentNullException(nameof(evaluationEvent)); - } - - if (evaluationEvent.FeatureDefinition == null) - { - throw new ArgumentException( - "Feature definition is required.", - nameof(evaluationEvent)); - } - - if (evaluationEvent.FeatureDefinition.Telemetry == null) - { - throw new ArgumentException( - "Feature definition telemetry configuration is required.", - nameof(evaluationEvent)); - } - } - - private static string ToString(VariantAssignmentReason reason) - { - Debug.Assert(reason != VariantAssignmentReason.None); - - const string DefaultWhenDisabled = "DefaultWhenDisabled"; - const string DefaultWhenEnabled = "DefaultWhenEnabled"; - const string User = "User"; - const string Group = "Group"; - const string Percentile = "Percentile"; - - return reason switch - { - VariantAssignmentReason.DefaultWhenDisabled => DefaultWhenDisabled, - VariantAssignmentReason.DefaultWhenEnabled => DefaultWhenEnabled, - VariantAssignmentReason.User => User, - VariantAssignmentReason.Group => Group, - VariantAssignmentReason.Percentile => Percentile, - _ => throw new ArgumentException("Invalid assignment reason.", nameof(reason)) - }; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/FeatureManagementBuilderExtensions.cs new file mode 100644 index 00000000..002d2bfc --- /dev/null +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/FeatureManagementBuilderExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.FeatureManagement.Telemetry.ApplicationInsights; + +namespace Microsoft.FeatureManagement +{ + /// + /// Extensions used to add feature management functionality. + /// + public static class FeatureManagementBuilderExtensions + { + /// + /// Adds the using to the feature management builder. + /// + /// The feature management builder. + /// The feature management builder. + public static IFeatureManagementBuilder AddApplicationInsightsTelemetryPublisher(this IFeatureManagementBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (builder.Services == null) + { + throw new ArgumentException($"The provided builder's services must not be null.", nameof(builder)); + } + + builder.Services.AddSingleton(); + + if (!builder.Services.Any((ServiceDescriptor d) => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(ApplicationInsightsHostedService))) + { + builder.Services.Insert(0, ServiceDescriptor.Singleton()); + } + + return builder; + } + } +} diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj index 708d97c5..359a596a 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs index e958b628..93cb60df 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs @@ -76,32 +76,5 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu return builder; } - - /// - /// Adds a telemetry publisher to the feature management system. - /// - /// The used to customize feature management functionality. - /// A that can be used to customize feature management functionality. - public static IFeatureManagementBuilder AddTelemetryPublisher(this IFeatureManagementBuilder builder) where T : ITelemetryPublisher - { - builder.AddTelemetryPublisher(sp => ActivatorUtilities.CreateInstance(sp, typeof(T)) as ITelemetryPublisher); - - return builder; - } - - private static IFeatureManagementBuilder AddTelemetryPublisher(this IFeatureManagementBuilder builder, Func factory) - { - builder.Services.Configure(options => - { - if (options.TelemetryPublisherFactories == null) - { - options.TelemetryPublisherFactories = new List>(); - } - - options.TelemetryPublisherFactories.Add(factory); - }); - - return builder; - } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagementOptions.cs b/src/Microsoft.FeatureManagement/FeatureManagementOptions.cs index 0e2c8eaf..04d6f04b 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementOptions.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementOptions.cs @@ -26,11 +26,5 @@ public class FeatureManagementOptions /// The default value is true. /// public bool IgnoreMissingFeatures { get; set; } = true; - - /// - /// Holds a collection of factories that can be used to create instances. - /// This avoids the need to add the publishers to the service collection. - /// - internal ICollection> TelemetryPublisherFactories { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 3b5b4cc2..da0d70ce 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -32,9 +32,13 @@ public sealed class FeatureManager : IFeatureManager, IVariantFeatureManager private readonly ConcurrentDictionary _contextualFeatureFilterCache; private readonly IEnumerable _featureFilters; private readonly IEnumerable _sessionManagers; - private readonly IEnumerable _telemetryPublishers; private readonly TargetingEvaluationOptions _assignerOptions; + /// + /// The activity source for feature management. + /// + private static readonly ActivitySource ActivitySource = new ActivitySource("Microsoft.FeatureManagement", "1.0.0"); + private class ConfigurationCacheItem { public IConfiguration Parameters { get; set; } @@ -58,7 +62,6 @@ public FeatureManager( _contextualFeatureFilterCache = new ConcurrentDictionary(); _featureFilters = Enumerable.Empty(); _sessionManagers = Enumerable.Empty(); - _telemetryPublishers = Enumerable.Empty(); _assignerOptions = new TargetingEvaluationOptions(); } @@ -100,20 +103,6 @@ public IEnumerable SessionManagers /// public ILogger Logger { get; init; } - /// - /// The collection of telemetry publishers. - /// - /// Thrown if it is set to null. - public IEnumerable TelemetryPublishers - { - get => _telemetryPublishers; - - init - { - _telemetryPublishers = value ?? throw new ArgumentNullException(nameof(value)); - } - } - /// /// The configuration reference for feature variants. /// @@ -262,6 +251,14 @@ private async ValueTask EvaluateFeature(string featur FeatureDefinition = await GetFeatureDefinition(feature).ConfigureAwait(false) }; + bool telemetryEnabled = evaluationEvent.FeatureDefinition?.Telemetry?.Enabled ?? false; + + // + // Only start an activity if telemetry is enabled for the feature + using Activity activity = telemetryEnabled + ? ActivitySource.StartActivity("FeatureEvaluation") + : null; + // // Determine Targeting Context TargetingContext targetingContext; @@ -314,15 +311,15 @@ private async ValueTask EvaluateFeature(string featur { string message; - if (useContext) + if (useContext) { message = $"A {nameof(TargetingContext)} required for variant assignment was not provided."; - } - else if (TargetingContextAccessor == null) + } + else if (TargetingContextAccessor == null) { message = $"No instance of {nameof(ITargetingContextAccessor)} could be found for variant assignment."; - } - else + } + else { message = $"No instance of {nameof(TargetingContext)} could be found using {nameof(ITargetingContextAccessor)} for variant assignment."; } @@ -347,7 +344,7 @@ private async ValueTask EvaluateFeature(string featur evaluationEvent.VariantAssignmentReason = VariantAssignmentReason.DefaultWhenEnabled; } - } + } evaluationEvent.Variant = variantDefinition != null ? GetVariantFromVariantDefinition(variantDefinition) : null; @@ -371,16 +368,61 @@ private async ValueTask EvaluateFeature(string featur await sessionManager.SetAsync(evaluationEvent.FeatureDefinition.Name, evaluationEvent.Enabled).ConfigureAwait(false); } - if (evaluationEvent.FeatureDefinition.Telemetry != null && - evaluationEvent.FeatureDefinition.Telemetry.Enabled) + // Only add an activity event if telemetry is enabled for the feature and the activity is valid + if (telemetryEnabled && + Activity.Current != null && + Activity.Current.IsAllDataRequested) { - PublishTelemetry(evaluationEvent, cancellationToken); + AddEvaluationActivityEvent(evaluationEvent); } } 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("feature_flag", DateTimeOffset.UtcNow, tags); + + Activity.Current.AddEvent(activityEvent); + } + private async ValueTask IsEnabledAsync(FeatureDefinition featureDefinition, TContext appContext, bool useAppContext, CancellationToken cancellationToken) { Debug.Assert(featureDefinition != null); @@ -521,7 +563,7 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) return enabled; } - + private async ValueTask GetFeatureDefinition(string feature) { FeatureDefinition featureDefinition = await _featureDefinitionProvider @@ -587,7 +629,7 @@ private ValueTask AssignVariantAsync(EvaluationEvent evaluati return new ValueTask( evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault(variant => + .FirstOrDefault(variant => variant.Name == user.Variant)); } } @@ -613,7 +655,7 @@ private ValueTask AssignVariantAsync(EvaluationEvent evaluati return new ValueTask( evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault(variant => + .FirstOrDefault(variant => variant.Name == group.Variant)); } } @@ -644,7 +686,7 @@ private ValueTask AssignVariantAsync(EvaluationEvent evaluati return new ValueTask( evaluationEvent.FeatureDefinition .Variants - .FirstOrDefault(variant => + .FirstOrDefault(variant => variant.Name == percentile.Variant)); } } @@ -708,7 +750,8 @@ private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName, Type { IFeatureFilterMetadata filter = _filterMetadataCache.GetOrAdd( $"{filterName}{Environment.NewLine}{appContextType?.FullName}", - (_) => { + (_) => + { IEnumerable matchingFilters = _featureFilters.Where(f => { @@ -786,7 +829,8 @@ private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filte ContextualFeatureFilterEvaluator filter = _contextualFeatureFilterCache.GetOrAdd( $"{filterName}{Environment.NewLine}{appContextType.FullName}", - (_) => { + (_) => + { IFeatureFilterMetadata metadata = GetFeatureFilterMetadata(filterName, appContextType); @@ -802,23 +846,6 @@ private ContextualFeatureFilterEvaluator GetContextualFeatureFilter(string filte return filter; } - private async void PublishTelemetry(EvaluationEvent evaluationEvent, CancellationToken cancellationToken) - { - if (!_telemetryPublishers.Any()) - { - Logger?.LogWarning("The feature declaration enabled telemetry but no telemetry publisher was registered."); - } - else - { - foreach (ITelemetryPublisher telemetryPublisher in _telemetryPublishers) - { - await telemetryPublisher.PublishEvent( - evaluationEvent, - cancellationToken); - } - } - } - private Variant GetVariantFromVariantDefinition(VariantDefinition variantDefinition) { IConfigurationSection variantConfiguration = null; diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index e3bd6025..2828ce79 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -39,6 +39,7 @@ + diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index a6651e59..f6412716 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; -using Microsoft.FeatureManagement.Telemetry; using System; using System.Collections.Generic; using System.Linq; @@ -49,10 +48,6 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec { FeatureFilters = sp.GetRequiredService>(), SessionManagers = sp.GetRequiredService>(), - TelemetryPublishers = sp.GetRequiredService>().Value?.TelemetryPublisherFactories? - .Select(factory => factory(sp)) - .ToList() ?? - Enumerable.Empty(), Cache = sp.GetRequiredService(), Logger = sp.GetRequiredService().CreateLogger(), Configuration = sp.GetService(), @@ -136,10 +131,6 @@ public static IFeatureManagementBuilder AddScopedFeatureManagement(this IService { FeatureFilters = sp.GetRequiredService>(), SessionManagers = sp.GetRequiredService>(), - TelemetryPublishers = sp.GetRequiredService>().Value?.TelemetryPublisherFactories? - .Select(factory => factory(sp)) - .ToList() ?? - Enumerable.Empty(), Cache = sp.GetRequiredService(), Logger = sp.GetRequiredService().CreateLogger(), Configuration = sp.GetService(), diff --git a/src/Microsoft.FeatureManagement/Telemetry/ITelemetryPublisher.cs b/src/Microsoft.FeatureManagement/Telemetry/ITelemetryPublisher.cs deleted file mode 100644 index 89f7a931..00000000 --- a/src/Microsoft.FeatureManagement/Telemetry/ITelemetryPublisher.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement.Telemetry -{ - /// - /// A publisher of telemetry events. - /// - public interface ITelemetryPublisher - { - /// - /// Handles an EvaluationEvent and publishes it to the configured telemetry channel. - /// - /// The event to publish. - /// A cancellation token. - /// ValueTask - public ValueTask PublishEvent(EvaluationEvent evaluationEvent, CancellationToken cancellationToken); - } -} diff --git a/src/Microsoft.FeatureManagement/TelemetryConfiguration.cs b/src/Microsoft.FeatureManagement/TelemetryConfiguration.cs index aee3fa1f..ebbb4829 100644 --- a/src/Microsoft.FeatureManagement/TelemetryConfiguration.cs +++ b/src/Microsoft.FeatureManagement/TelemetryConfiguration.cs @@ -1,5 +1,6 @@ using Microsoft.FeatureManagement.Telemetry; using System.Collections.Generic; +using System.Diagnostics; namespace Microsoft.FeatureManagement { @@ -9,7 +10,7 @@ namespace Microsoft.FeatureManagement public class TelemetryConfiguration { /// - /// A flag to enable or disable sending events to registered s. + /// A flag to enable or disable sending events as s. /// public bool Enabled { get; set; } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 0af03f08..478501d6 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -6,9 +6,9 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Telemetry; -using Microsoft.FeatureManagement.Tests; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -1335,6 +1335,8 @@ public class FeatureManagementTelemetryTest [Fact] public async Task TelemetryPublishing() { + int currentTest = 0; + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); var services = new ServiceCollection(); @@ -1342,135 +1344,202 @@ public async Task TelemetryPublishing() var targetingContextAccessor = new OnDemandTargetingContextAccessor(); services.AddSingleton(targetingContextAccessor) .AddSingleton(config) - .AddFeatureManagement() - .AddTelemetryPublisher(); + .AddFeatureManagement(); ServiceProvider serviceProvider = services.BuildServiceProvider(); FeatureManager featureManager = (FeatureManager)serviceProvider.GetRequiredService(); - TestTelemetryPublisher testPublisher = (TestTelemetryPublisher)featureManager.TelemetryPublishers.First(); CancellationToken cancellationToken = CancellationToken.None; - // Test a feature with telemetry disabled - bool result = await featureManager.IsEnabledAsync(Features.OnTestFeature, cancellationToken); + using Activity testActivity = new Activity("TestActivity").Start(); - Assert.True(result); - Assert.Null(testPublisher.evaluationEventCache); + // Start listener + using ActivityListener activityListener = new ActivityListener + { + ShouldListenTo = (activitySource) => activitySource.Name == "Microsoft.FeatureManagement", + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + ActivityStopped = (activity) => + { + // Stop other tests from asserting + if (activity.ParentId != testActivity.Id) + { + return; + } - // Test telemetry cases - result = await featureManager.IsEnabledAsync(Features.OnTelemetryTestFeature, cancellationToken); + ActivityEvent? evaluationEventNullable = activity.Events.FirstOrDefault((activityEvent) => activityEvent.Name == "feature_flag"); - Assert.True(result); - Assert.Equal(Features.OnTelemetryTestFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); - Assert.Equal("EtagValue", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Etag"]); - Assert.Equal("LabelValue", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Label"]); - Assert.Equal("Tag1Value", testPublisher.evaluationEventCache.FeatureDefinition.Telemetry.Metadata["Tags.Tag1"]); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason); + if (evaluationEventNullable != null && evaluationEventNullable.Value.Tags.Any()) + { + ActivityEvent evaluationEvent = evaluationEventNullable.Value; + + string featureName = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "FeatureName").Value?.ToString(); + string targetingId = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "TargetingId").Value?.ToString(); + string variantName = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Variant").Value?.ToString(); + string enabled = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Enabled").Value?.ToString(); + string variantAssignmentReason = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "VariantAssignmentReason").Value?.ToString(); + string version = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Version").Value?.ToString(); + string etag = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Etag").Value?.ToString(); + string label = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Label").Value?.ToString(); + string firstTag = evaluationEvent.Tags.FirstOrDefault(kvp => kvp.Key == "Tags.Tag1").Value?.ToString(); + + // Test telemetry cases + switch (featureName) + { + case Features.OnTelemetryTestFeature: + Assert.Equal(1, currentTest); + currentTest = 0; + Assert.Equal("True", enabled); + Assert.Equal("EtagValue", etag); + Assert.Equal("LabelValue", label); + Assert.Equal("Tag1Value", firstTag); + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.None.ToString(), variantAssignmentReason); + break; + + case Features.OffTelemetryTestFeature: + Assert.Equal(2, currentTest); + currentTest = 0; + Assert.Equal("False", enabled); + Assert.Equal(VariantAssignmentReason.None.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureDefaultEnabled: + Assert.Equal(3, currentTest); + currentTest = 0; + Assert.Equal("True", enabled); + Assert.Equal("Medium", variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureDefaultDisabled: + Assert.Equal(4, currentTest); + currentTest = 0; + Assert.Equal("False", enabled); + Assert.Equal("Small", variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeaturePercentileOn: + Assert.Equal(5, currentTest); + currentTest = 0; + Assert.Equal("Big", variantName); + Assert.Equal("Marsha", targetingId); + Assert.Equal(VariantAssignmentReason.Percentile.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeaturePercentileOff: + Assert.Equal(6, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureAlwaysOff: + Assert.Equal(7, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureUser: + Assert.Equal(8, currentTest); + currentTest = 0; + Assert.Equal("Small", variantName); + Assert.Equal(VariantAssignmentReason.User.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureGroup: + Assert.Equal(9, currentTest); + currentTest = 0; + Assert.Equal("Small", variantName); + Assert.Equal(VariantAssignmentReason.Group.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureNoVariants: + Assert.Equal(10, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.None.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureNoAllocation: + Assert.Equal(11, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled.ToString(), variantAssignmentReason); + break; + + case Features.VariantFeatureAlwaysOffNoAllocation: + Assert.Equal(12, currentTest); + currentTest = 0; + Assert.Null(variantName); + Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled.ToString(), variantAssignmentReason); + break; + + default: + throw new Exception("Unexpected feature name"); + } + } + } + }; + ActivitySource.AddActivityListener(activityListener); - result = await featureManager.IsEnabledAsync(Features.OffTelemtryTestFeature, cancellationToken); + currentTest = 1; + await featureManager.IsEnabledAsync(Features.OnTelemetryTestFeature, cancellationToken); + Assert.Equal(0, currentTest); - Assert.False(result); - Assert.Equal(Features.OffTelemtryTestFeature, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); - Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason); + currentTest = 2; + await featureManager.IsEnabledAsync(Features.OffTelemetryTestFeature, cancellationToken); + Assert.Equal(0, currentTest); // Test variant cases - result = await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); - - Assert.True(result); - Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); - Assert.Equal("Medium", testPublisher.evaluationEventCache.Variant.Name); + currentTest = 3; + await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); + Assert.Equal(0, currentTest); - Variant variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); - - Assert.True(testPublisher.evaluationEventCache.Enabled); - Assert.Equal(Features.VariantFeatureDefaultEnabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - - result = await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultDisabled, cancellationToken); - - Assert.False(result); - Assert.Equal(Features.VariantFeatureDefaultDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(result, testPublisher.evaluationEventCache.Enabled); - Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultDisabled, cancellationToken); - - Assert.False(testPublisher.evaluationEventCache.Enabled); - Assert.Equal(Features.VariantFeatureDefaultDisabled, testPublisher.evaluationEventCache.FeatureDefinition.Name); - Assert.Equal(variantResult.Name, testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); + currentTest = 4; + await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultDisabled, cancellationToken); + Assert.Equal(0, currentTest); targetingContextAccessor.Current = new TargetingContext { UserId = "Marsha", Groups = new List { "Group1" } }; + currentTest = 5; + await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken); + Assert.Equal(0, currentTest); - variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOn, cancellationToken); - Assert.Equal("Big", variantResult.Name); - Assert.Equal("Big", testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal("Marsha", testPublisher.evaluationEventCache.TargetingContext.UserId); - Assert.Equal(VariantAssignmentReason.Percentile, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken); - Assert.Null(variantResult); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOff, cancellationToken); - Assert.Null(variantResult); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken); - Assert.Equal("Small", variantResult.Name); - Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal(VariantAssignmentReason.User, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken); - Assert.Equal("Small", variantResult.Name); - Assert.Equal("Small", testPublisher.evaluationEventCache.Variant.Name); - Assert.Equal(VariantAssignmentReason.Group, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureNoVariants, cancellationToken); - Assert.Null(variantResult); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.None, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken); - Assert.Null(variantResult); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.DefaultWhenEnabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - - variantResult = await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, cancellationToken); - Assert.Null(variantResult); - Assert.Null(testPublisher.evaluationEventCache.Variant); - Assert.Equal(VariantAssignmentReason.DefaultWhenDisabled, testPublisher.evaluationEventCache.VariantAssignmentReason); - } + currentTest = 6; + await featureManager.GetVariantAsync(Features.VariantFeaturePercentileOff, cancellationToken); + Assert.Equal(0, currentTest); - [Fact] - public async Task TelemetryPublishingNullPublisher() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + currentTest = 7; + await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOff, cancellationToken); + Assert.Equal(0, currentTest); - var services = new ServiceCollection(); + currentTest = 8; + await featureManager.GetVariantAsync(Features.VariantFeatureUser, cancellationToken); + Assert.Equal(0, currentTest); - services - .AddSingleton(config) - .AddFeatureManagement(); + currentTest = 9; + await featureManager.GetVariantAsync(Features.VariantFeatureGroup, cancellationToken); + Assert.Equal(0, currentTest); - ServiceProvider serviceProvider = services.BuildServiceProvider(); + currentTest = 10; + await featureManager.GetVariantAsync(Features.VariantFeatureNoVariants, cancellationToken); + Assert.Equal(0, currentTest); - FeatureManager featureManager = (FeatureManager)serviceProvider.GetRequiredService(); + currentTest = 11; + await featureManager.GetVariantAsync(Features.VariantFeatureNoAllocation, cancellationToken); + Assert.Equal(0, currentTest); - // Test telemetry enabled feature with no telemetry publisher - bool result = await featureManager.IsEnabledAsync(Features.OnTelemetryTestFeature, CancellationToken.None); + currentTest = 12; + await featureManager.GetVariantAsync(Features.VariantFeatureAlwaysOffNoAllocation, 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); Assert.True(result); } diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index 6dc6833b..51eef015 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -29,6 +29,6 @@ static class Features public const string VariantFeatureInvalidFromTo = "VariantFeatureInvalidFromTo"; public const string VariantImplementationFeature = "VariantImplementationFeature"; public const string OnTelemetryTestFeature = "OnTelemetryTestFeature"; - public const string OffTelemtryTestFeature = "OffTelemetryTestFeature"; + public const string OffTelemetryTestFeature = "OffTelemetryTestFeature"; } } diff --git a/tests/Tests.FeatureManagement/TestTelemetryPublisher.cs b/tests/Tests.FeatureManagement/TestTelemetryPublisher.cs deleted file mode 100644 index 088941b5..00000000 --- a/tests/Tests.FeatureManagement/TestTelemetryPublisher.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.FeatureManagement.Telemetry; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.FeatureManagement.Tests -{ - public class TestTelemetryPublisher : ITelemetryPublisher - { - public EvaluationEvent evaluationEventCache { get; private set; } - - public ValueTask PublishEvent(EvaluationEvent evaluationEvent, CancellationToken cancellationToken) - { - evaluationEventCache = evaluationEvent; - - return new ValueTask(); - } - } -} \ No newline at end of file