From e7d40c792b04e214c0da08e5feabe25d816a483e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:04:39 +0000 Subject: [PATCH 01/17] Added metrics hook. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Hooks/MetricsConstants.cs | 20 +++++ src/OpenFeature/Hooks/MetricsHook.cs | 100 ++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/OpenFeature/Hooks/MetricsConstants.cs create mode 100644 src/OpenFeature/Hooks/MetricsHook.cs diff --git a/src/OpenFeature/Hooks/MetricsConstants.cs b/src/OpenFeature/Hooks/MetricsConstants.cs new file mode 100644 index 00000000..7b020fb3 --- /dev/null +++ b/src/OpenFeature/Hooks/MetricsConstants.cs @@ -0,0 +1,20 @@ +namespace OpenFeature.Hooks; + +internal static class MetricsConstants +{ + internal const string ActiveCountName = "feature_flag.evaluation_active_count"; + internal const string RequestsTotalName = "feature_flag.evaluation_requests_total"; + internal const string SuccessTotalName = "feature_flag.evaluation_success_total"; + internal const string ErrorTotalName = "feature_flag.evaluation_error_total"; + + internal const string ActiveDescription = "active flag evaluations counter"; + internal const string RequestsDescription = "feature flag evaluation request counter"; + internal const string SuccessDescription = "feature flag evaluation success counter"; + internal const string ErrorDescription = "feature flag evaluation error counter"; + + internal const string KeyAttr = "key"; + internal const string ProviderNameAttr = "provider_name"; + internal const string VariantAttr = "variant"; + internal const string ReasonAttr = "reason"; + internal const string ExceptionAttr = "exception"; +} diff --git a/src/OpenFeature/Hooks/MetricsHook.cs b/src/OpenFeature/Hooks/MetricsHook.cs new file mode 100644 index 00000000..99fe4c9b --- /dev/null +++ b/src/OpenFeature/Hooks/MetricsHook.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.Hooks; + +/// +/// Represents a hook for capturing metrics related to flag evaluations. +/// The meter instrumentation name is "OpenFeature". +/// +public class MetricsHook : Hook +{ + private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName(); + private static readonly string InstrumentationName = AssemblyName.Name ?? "OpenFeature"; + private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString() ?? "1.0.0"; + + private readonly UpDownCounter _evaluationActiveUpDownCounter; + private readonly Counter _evaluationRequestCounter; + private readonly Counter _evaluationSuccessCounter; + private readonly Counter _evaluationErrorCounter; + + /// + /// Initializes a new instance of the class. + /// + public MetricsHook() + { + var meter = new Meter(InstrumentationName, InstrumentationVersion); + + this._evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription); + this._evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription); + this._evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription); + this._evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription); + } + + /// + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var tagList = new TagList + { + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name } + }; + + this._evaluationActiveUpDownCounter.Add(1, tagList); + this._evaluationRequestCounter.Add(1, tagList); + + return base.BeforeAsync(context, hints, cancellationToken); + } + + + /// + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var tagList = new TagList + { + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, + { MetricsConstants.VariantAttr, details.Variant ?? details.Value?.ToString() }, + { MetricsConstants.ReasonAttr, details.Reason ?? "UNKNOWN" } + }; + + this._evaluationSuccessCounter.Add(1, tagList); + + return base.AfterAsync(context, details, hints, cancellationToken); + } + + /// + public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var tagList = new TagList + { + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, + { MetricsConstants.ExceptionAttr, error.Message } + }; + + this._evaluationErrorCounter.Add(1, tagList); + + return base.ErrorAsync(context, error, hints, cancellationToken); + } + + /// + public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var tagList = new TagList + { + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name } + }; + + this._evaluationActiveUpDownCounter.Add(-1, tagList); + + return base.FinallyAsync(context, hints, cancellationToken); + } +} From 46e5711481665e337535b1f373744b5e9e506ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:29:10 +0000 Subject: [PATCH 02/17] Adding the traces hook. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Hooks/TracingConstants.cs | 9 ++++ src/OpenFeature/Hooks/TracingHook.cs | 54 +++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/OpenFeature/Hooks/TracingConstants.cs create mode 100644 src/OpenFeature/Hooks/TracingHook.cs diff --git a/src/OpenFeature/Hooks/TracingConstants.cs b/src/OpenFeature/Hooks/TracingConstants.cs new file mode 100644 index 00000000..27be6d7c --- /dev/null +++ b/src/OpenFeature/Hooks/TracingConstants.cs @@ -0,0 +1,9 @@ +namespace OpenFeature.Hooks; + +internal static class TracingConstants +{ + internal const string AttributeExceptionEventName = "exception"; + internal const string AttributeExceptionType = "exception.type"; + internal const string AttributeExceptionMessage = "exception.message"; + internal const string AttributeExceptionStacktrace = "exception.stacktrace"; +} diff --git a/src/OpenFeature/Hooks/TracingHook.cs b/src/OpenFeature/Hooks/TracingHook.cs new file mode 100644 index 00000000..037a00c9 --- /dev/null +++ b/src/OpenFeature/Hooks/TracingHook.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.Hooks; + +/// +/// Stub. +/// +public class TracingHook : Hook +{ + /// + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + Activity.Current? + .SetTag("feature_flag.key", details.FlagKey) + .SetTag("feature_flag.variant", details.Variant) + .SetTag("feature_flag.provider_name", context.ProviderMetadata.Name) + .AddEvent(new ActivityEvent("feature_flag", tags: new ActivityTagsCollection + { + ["feature_flag.key"] = details.FlagKey, + ["feature_flag.variant"] = details.Variant, + ["feature_flag.provider_name"] = context.ProviderMetadata.Name + })); + + return default; + } + + /// + public override ValueTask ErrorAsync(HookContext context, System.Exception error, + IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { +#if NET9_0_OR_GREATER + // For dotnet9 we should use the new API https://learn.microsoft.com/en-gb/dotnet/api/system.diagnostics.activity.addexception?view=net-9.0 + // Activity.Current?.AddException(error); +#else + var tagsCollection = new ActivityTagsCollection + { + { TracingConstants.AttributeExceptionType, error.GetType().FullName }, + { TracingConstants.AttributeExceptionStacktrace, error.ToString() }, + }; + if (!string.IsNullOrWhiteSpace(error.Message)) + { + tagsCollection.Add(TracingConstants.AttributeExceptionMessage, error.Message); + } + + Activity.Current?.AddEvent(new ActivityEvent(TracingConstants.AttributeExceptionEventName, default, tagsCollection)); +#endif + return default; + } +} From ced2d341650cfc877f236305a4bfa753c4692329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:35:20 +0000 Subject: [PATCH 03/17] Adding Metrics Hook tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- Directory.Packages.props | 9 +- .../Hooks/MetricsHookTests.cs | 177 ++++++++++++++++++ .../OpenFeature.Tests.csproj | 2 + 3 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 test/OpenFeature.Tests/Hooks/MetricsHookTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1dbc878a..08b892ce 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,9 +1,7 @@ - true - @@ -15,7 +13,6 @@ - @@ -25,6 +22,8 @@ + + @@ -32,9 +31,7 @@ - - - + \ No newline at end of file diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs new file mode 100644 index 00000000..1379ab1b --- /dev/null +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using OpenFeature.Hooks; +using OpenFeature.Model; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using Xunit; + +namespace OpenFeature.Tests.Hooks; + +public class MetricsHookTest +{ + [Fact] + public void After_Test() + { + // Arrange metrics collector + var exportedItems = new List(); + Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .ConfigureResource(r => r.AddService("openfeature")) + .AddInMemoryExporter(exportedItems, + option => option.PeriodicExportingMetricReaderOptions = + new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) + .Build(); + + // Arrange + const string metricName = "feature_flag.evaluation_success_total"; + var otelHook = new MetricsHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + var hookTask = otelHook.AfterAsync(ctx, + new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), + new Dictionary()); + // Wait for the metrics to be exported + Thread.Sleep(150); + + // Assert + Assert.True(hookTask.IsCompleted); + + // Assert metrics + Assert.NotEmpty(exportedItems); + + // check if the metric is present in the exported items + var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + Assert.NotNull(metric); + + var noOtherMetric = exportedItems.All(m => m.Name == metricName); + Assert.True(noOtherMetric); + } + + [Fact] + public void Error_Test() + { + // Arrange metrics collector + var exportedItems = new List(); + Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .ConfigureResource(r => r.AddService("openfeature")) + .AddInMemoryExporter(exportedItems, + option => option.PeriodicExportingMetricReaderOptions = + new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) + .Build(); + + // Arrange + const string metricName = "feature_flag.evaluation_error_total"; + var otelHook = new MetricsHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + var hookTask = otelHook.ErrorAsync(ctx, new Exception(), new Dictionary()); + // Wait for the metrics to be exported + Thread.Sleep(150); + + // Assert + Assert.True(hookTask.IsCompleted); + + // Assert metrics + Assert.NotEmpty(exportedItems); + + // check if the metric is present in the exported items + var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + Assert.NotNull(metric); + + var noOtherMetric = exportedItems.All(m => m.Name == metricName); + Assert.True(noOtherMetric); + } + + [Fact] + public void Finally_Test() + { + // Arrange metrics collector + var exportedItems = new List(); + Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .ConfigureResource(r => r.AddService("openfeature")) + .AddInMemoryExporter(exportedItems, + option => option.PeriodicExportingMetricReaderOptions = + new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) + .Build(); + + // Arrange + const string metricName = "feature_flag.evaluation_active_count"; + var otelHook = new MetricsHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + var hookTask = otelHook.FinallyAsync(ctx, new Dictionary()); + // Wait for the metrics to be exported + Thread.Sleep(150); + + // Assert + Assert.True(hookTask.IsCompleted); + + // Assert metrics + Assert.NotEmpty(exportedItems); + + // check if the metric feature_flag.evaluation_success_total is present in the exported items + var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + Assert.NotNull(metric); + + var noOtherMetric = exportedItems.All(m => m.Name == metricName); + Assert.True(noOtherMetric); + } + + [Fact] + public void Before_Test() + { + // Arrange metrics collector + var exportedItems = new List(); + Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .ConfigureResource(r => r.AddService("openfeature")) + .AddInMemoryExporter(exportedItems, + option => option.PeriodicExportingMetricReaderOptions = + new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) + .Build(); + + // Arrange + const string metricName1 = "feature_flag.evaluation_active_count"; + const string metricName2 = "feature_flag.evaluation_requests_total"; + var otelHook = new MetricsHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + var hookTask = otelHook.BeforeAsync(ctx, new Dictionary()); + // Wait for the metrics to be exported + Thread.Sleep(150); + + // Assert + Assert.True(hookTask.IsCompleted); + + // Assert metrics + Assert.NotEmpty(exportedItems); + + // check if the metric is present in the exported items + var metric1 = exportedItems.FirstOrDefault(m => m.Name == metricName1); + Assert.NotNull(metric1); + + var metric2 = exportedItems.FirstOrDefault(m => m.Name == metricName2); + Assert.NotNull(metric2); + + var noOtherMetric = exportedItems.All(m => m.Name == metricName1 || m.Name == metricName2); + Assert.True(noOtherMetric); + } +} diff --git a/test/OpenFeature.Tests/OpenFeature.Tests.csproj b/test/OpenFeature.Tests/OpenFeature.Tests.csproj index bfadbf9b..63ca19e4 100644 --- a/test/OpenFeature.Tests/OpenFeature.Tests.csproj +++ b/test/OpenFeature.Tests/OpenFeature.Tests.csproj @@ -19,6 +19,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive From 633859c36c2d67bdf7543df55dead54636c0207d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:42:19 +0000 Subject: [PATCH 04/17] Adding tracing hook tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 test/OpenFeature.Tests/Hooks/TracingHookTests.cs diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs new file mode 100644 index 00000000..927e0204 --- /dev/null +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -0,0 +1,182 @@ +using System.Collections.Generic; +using System.Diagnostics; +using OpenFeature.Hooks; +using OpenFeature.Model; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenFeature.Tests.Hooks; + +public class TracingHookTest +{ + [Fact] + public void TestAfter() + { + // List that will be populated with the traces by InMemoryExporter + var exportedItems = new List(); + + // Create a new in-memory exporter + var exporter = new InMemoryExporter(exportedItems); + + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("my-tracer") + .ConfigureResource(r => r.AddService("inmemory-test")) + .AddInMemoryExporter(exportedItems) + .Build(); + + + var tracer = tracerProvider.GetTracer("my-tracer"); + + var span = tracer.StartActiveSpan("my-span"); + + var otelHook = new TracingHook(); + + var evaluationContext = EvaluationContext.Empty; + + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + var hookTask = otelHook.AfterAsync(ctx, + new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), + new Dictionary()); + + Assert.True(hookTask.IsCompleted); + + span.End(); + + Assert.Single(exportedItems); + + var rootSpan = exportedItems[0]; + + Assert.Single(rootSpan.Events); + + var eventsEnum = rootSpan.Events.GetEnumerator(); + eventsEnum.MoveNext(); + + ActivityEvent ev = eventsEnum.Current; + Assert.Equal("feature_flag", ev.Name); + + Assert.Contains(new KeyValuePair("feature_flag.key", "my-flag"), ev.Tags); + Assert.Contains(new KeyValuePair("feature_flag.variant", "default"), ev.Tags); + Assert.Contains(new KeyValuePair("feature_flag.provider_name", "my-provider"), ev.Tags); + } + + [Fact] + public void TestAfterNoSpan() + { + // List that will be populated with the traces by InMemoryExporter + var exportedItems = new List(); + + // Create a new in-memory exporter + var exporter = new InMemoryExporter(exportedItems); + + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("my-tracer") + .ConfigureResource(r => r.AddService("inmemory-test")) + .AddInMemoryExporter(exportedItems) + .Build(); + + + var tracer = tracerProvider.GetTracer("my-tracer"); + + var otelHook = new TracingHook(); + + var evaluationContext = EvaluationContext.Empty; + + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + var hookTask = otelHook.AfterAsync(ctx, + new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), + new Dictionary()); + + Assert.True(hookTask.IsCompleted); + + Assert.Empty(exportedItems); + } + + [Fact] + public void TestError() + { + // List that will be populated with the traces by InMemoryExporter + var exportedItems = new List(); + + // Create a new in-memory exporter + var exporter = new InMemoryExporter(exportedItems); + + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("my-tracer") + .ConfigureResource(r => r.AddService("inmemory-test")) + .AddInMemoryExporter(exportedItems) + .Build(); + + + var tracer = tracerProvider.GetTracer("my-tracer"); + + var span = tracer.StartActiveSpan("my-span"); + + var otelHook = new TracingHook(); + + var evaluationContext = EvaluationContext.Empty; + + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + var hookTask = otelHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + new Dictionary()); + + Assert.True(hookTask.IsCompleted); + + span.End(); + + Assert.Single(exportedItems); + + var rootSpan = exportedItems[0]; + + Assert.Single(rootSpan.Events); + + var enumerator = rootSpan.Events.GetEnumerator(); + enumerator.MoveNext(); + var ev = enumerator.Current; + + Assert.Equal("exception", ev.Name); + + Assert.Contains(new KeyValuePair("exception.message", "unexpected error"), ev.Tags); + } + + [Fact] + public void TestErrorNoSpan() + { + // List that will be populated with the traces by InMemoryExporter + var exportedItems = new List(); + + // Create a new in-memory exporter + var exporter = new InMemoryExporter(exportedItems); + + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("my-tracer") + .ConfigureResource(r => r.AddService("inmemory-test")) + .AddInMemoryExporter(exportedItems) + .Build(); + + + var tracer = tracerProvider.GetTracer("my-tracer"); + + var otelHook = new TracingHook(); + + var evaluationContext = EvaluationContext.Empty; + + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + var hookTask = otelHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + new Dictionary()); + + Assert.True(hookTask.IsCompleted); + + Assert.Empty(exportedItems); + } +} From 6d4378f907a812df25b4af5b65bbc4efbe22ff04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:43:40 +0000 Subject: [PATCH 05/17] Fix typos. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 927e0204..7bc1e8cd 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -23,7 +23,7 @@ public void TestAfter() var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("inmemory-test")) + .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); @@ -32,14 +32,14 @@ public void TestAfter() var span = tracer.StartActiveSpan("my-span"); - var otelHook = new TracingHook(); + var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = otelHook.AfterAsync(ctx, + var hookTask = tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); @@ -75,21 +75,21 @@ public void TestAfterNoSpan() var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("inmemory-test")) + .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); var tracer = tracerProvider.GetTracer("my-tracer"); - var otelHook = new TracingHook(); + var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = otelHook.AfterAsync(ctx, + var hookTask = tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); @@ -109,7 +109,7 @@ public void TestError() var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("inmemory-test")) + .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); @@ -118,14 +118,14 @@ public void TestError() var span = tracer.StartActiveSpan("my-span"); - var otelHook = new TracingHook(); + var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = otelHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + var hookTask = tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), new Dictionary()); Assert.True(hookTask.IsCompleted); @@ -158,21 +158,21 @@ public void TestErrorNoSpan() var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("inmemory-test")) + .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); var tracer = tracerProvider.GetTracer("my-tracer"); - var otelHook = new TracingHook(); + var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = otelHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + var hookTask = tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), new Dictionary()); Assert.True(hookTask.IsCompleted); From 8a32ca99205cee55dc47eb4c93dd213bc9ae6959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 10:55:04 +0000 Subject: [PATCH 06/17] Simplify code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 7bc1e8cd..7c047508 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using OpenFeature.Hooks; using OpenFeature.Model; using OpenTelemetry; @@ -48,15 +49,10 @@ public void TestAfter() span.End(); Assert.Single(exportedItems); - - var rootSpan = exportedItems[0]; + var rootSpan = exportedItems.First(); Assert.Single(rootSpan.Events); - - var eventsEnum = rootSpan.Events.GetEnumerator(); - eventsEnum.MoveNext(); - - ActivityEvent ev = eventsEnum.Current; + ActivityEvent ev = rootSpan.Events.First(); Assert.Equal("feature_flag", ev.Name); Assert.Contains(new KeyValuePair("feature_flag.key", "my-flag"), ev.Tags); @@ -133,14 +129,10 @@ public void TestError() span.End(); Assert.Single(exportedItems); - - var rootSpan = exportedItems[0]; + var rootSpan = exportedItems.First(); Assert.Single(rootSpan.Events); - - var enumerator = rootSpan.Events.GetEnumerator(); - enumerator.MoveNext(); - var ev = enumerator.Current; + var ev = rootSpan.Events.First(); Assert.Equal("exception", ev.Name); From 629b9000f54758aac06d6e96ec03a923237c8dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:04:46 +0000 Subject: [PATCH 07/17] Cleanup tracing hook tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 107 +++--------------- 1 file changed, 16 insertions(+), 91 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 7c047508..f41c523d 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using OpenFeature.Hooks; using OpenFeature.Model; using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Xunit; @@ -14,40 +14,36 @@ namespace OpenFeature.Tests.Hooks; public class TracingHookTest { [Fact] - public void TestAfter() + public async Task TestAfter() { + // Arrange // List that will be populated with the traces by InMemoryExporter var exportedItems = new List(); // Create a new in-memory exporter - var exporter = new InMemoryExporter(exportedItems); - var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); - var tracer = tracerProvider.GetTracer("my-tracer"); - var span = tracer.StartActiveSpan("my-span"); - var tracingHook = new TracingHook(); - var evaluationContext = EvaluationContext.Empty; - var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = tracingHook.AfterAsync(ctx, + // Act + var span = tracer.StartActiveSpan("my-span"); + await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); - - Assert.True(hookTask.IsCompleted); - span.End(); + tracerProvider.ForceFlush(); + + // Assert Assert.Single(exportedItems); var rootSpan = exportedItems.First(); @@ -61,73 +57,35 @@ public void TestAfter() } [Fact] - public void TestAfterNoSpan() + public async Task TestError() { + // Arrange // List that will be populated with the traces by InMemoryExporter var exportedItems = new List(); // Create a new in-memory exporter - var exporter = new InMemoryExporter(exportedItems); - var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") .ConfigureResource(r => r.AddService("in-memory-test")) .AddInMemoryExporter(exportedItems) .Build(); - var tracer = tracerProvider.GetTracer("my-tracer"); var tracingHook = new TracingHook(); - var evaluationContext = EvaluationContext.Empty; - var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - var hookTask = tracingHook.AfterAsync(ctx, - new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()); - - Assert.True(hookTask.IsCompleted); - - Assert.Empty(exportedItems); - } - - [Fact] - public void TestError() - { - // List that will be populated with the traces by InMemoryExporter - var exportedItems = new List(); - - // Create a new in-memory exporter - var exporter = new InMemoryExporter(exportedItems); - - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("in-memory-test")) - .AddInMemoryExporter(exportedItems) - .Build(); - - - var tracer = tracerProvider.GetTracer("my-tracer"); - + // Act var span = tracer.StartActiveSpan("my-span"); - - var tracingHook = new TracingHook(); - - var evaluationContext = EvaluationContext.Empty; - - var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, - new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - - var hookTask = tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + await tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), new Dictionary()); - - Assert.True(hookTask.IsCompleted); - span.End(); + tracerProvider.ForceFlush(); + + // Assert Assert.Single(exportedItems); var rootSpan = exportedItems.First(); @@ -138,37 +96,4 @@ public void TestError() Assert.Contains(new KeyValuePair("exception.message", "unexpected error"), ev.Tags); } - - [Fact] - public void TestErrorNoSpan() - { - // List that will be populated with the traces by InMemoryExporter - var exportedItems = new List(); - - // Create a new in-memory exporter - var exporter = new InMemoryExporter(exportedItems); - - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("in-memory-test")) - .AddInMemoryExporter(exportedItems) - .Build(); - - - var tracer = tracerProvider.GetTracer("my-tracer"); - - var tracingHook = new TracingHook(); - - var evaluationContext = EvaluationContext.Empty; - - var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, - new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); - - var hookTask = tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), - new Dictionary()); - - Assert.True(hookTask.IsCompleted); - - Assert.Empty(exportedItems); - } } From 766caa58f6abc5cdd367e243a2f02129f4a1e03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:13:47 +0000 Subject: [PATCH 08/17] More test cleanup. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/MetricsHookTests.cs | 132 +++++++----------- 1 file changed, 50 insertions(+), 82 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index 1379ab1b..be6dd097 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; +using System.Threading.Tasks; using OpenFeature.Hooks; using OpenFeature.Model; using OpenTelemetry; @@ -11,167 +11,135 @@ namespace OpenFeature.Tests.Hooks; -public class MetricsHookTest +[CollectionDefinition(nameof(MetricsHookTest), DisableParallelization = true)] +public class MetricsHookTest : IDisposable { - [Fact] - public void After_Test() + private readonly List _exportedItems; + private readonly MeterProvider _meterProvider; + + public MetricsHookTest() { // Arrange metrics collector - var exportedItems = new List(); - Sdk.CreateMeterProviderBuilder() + this._exportedItems = []; + this._meterProvider = Sdk.CreateMeterProviderBuilder() .AddMeter("*") - .ConfigureResource(r => r.AddService("openfeature")) - .AddInMemoryExporter(exportedItems, + .ConfigureResource(r => r.AddService("open-feature")) + .AddInMemoryExporter(this._exportedItems, option => option.PeriodicExportingMetricReaderOptions = new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) .Build(); + } + +#pragma warning disable CA1816 + public void Dispose() + { + this._meterProvider.Shutdown(); + } +#pragma warning restore CA1816 + [Fact] + public async Task After_Test() + { // Arrange const string metricName = "feature_flag.evaluation_success_total"; - var otelHook = new MetricsHook(); + var metricsHook = new MetricsHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var hookTask = otelHook.AfterAsync(ctx, + await metricsHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); - // Wait for the metrics to be exported - Thread.Sleep(150); - - // Assert - Assert.True(hookTask.IsCompleted); + this._meterProvider.ForceFlush(); // Assert metrics - Assert.NotEmpty(exportedItems); + Assert.NotEmpty(this._exportedItems); // check if the metric is present in the exported items - var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName); Assert.NotNull(metric); - var noOtherMetric = exportedItems.All(m => m.Name == metricName); + var noOtherMetric = this._exportedItems.All(m => m.Name == metricName); Assert.True(noOtherMetric); } [Fact] - public void Error_Test() + public async Task Error_Test() { - // Arrange metrics collector - var exportedItems = new List(); - Sdk.CreateMeterProviderBuilder() - .AddMeter("*") - .ConfigureResource(r => r.AddService("openfeature")) - .AddInMemoryExporter(exportedItems, - option => option.PeriodicExportingMetricReaderOptions = - new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) - .Build(); - // Arrange const string metricName = "feature_flag.evaluation_error_total"; - var otelHook = new MetricsHook(); + var metricsHook = new MetricsHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var hookTask = otelHook.ErrorAsync(ctx, new Exception(), new Dictionary()); - // Wait for the metrics to be exported - Thread.Sleep(150); - - // Assert - Assert.True(hookTask.IsCompleted); + await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()); + this._meterProvider.ForceFlush(); // Assert metrics - Assert.NotEmpty(exportedItems); + Assert.NotEmpty(this._exportedItems); // check if the metric is present in the exported items - var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName); Assert.NotNull(metric); - var noOtherMetric = exportedItems.All(m => m.Name == metricName); + var noOtherMetric = this._exportedItems.All(m => m.Name == metricName); Assert.True(noOtherMetric); } [Fact] - public void Finally_Test() + public async Task Finally_Test() { - // Arrange metrics collector - var exportedItems = new List(); - Sdk.CreateMeterProviderBuilder() - .AddMeter("*") - .ConfigureResource(r => r.AddService("openfeature")) - .AddInMemoryExporter(exportedItems, - option => option.PeriodicExportingMetricReaderOptions = - new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) - .Build(); - // Arrange const string metricName = "feature_flag.evaluation_active_count"; - var otelHook = new MetricsHook(); + var metricsHook = new MetricsHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var hookTask = otelHook.FinallyAsync(ctx, new Dictionary()); - // Wait for the metrics to be exported - Thread.Sleep(150); - - // Assert - Assert.True(hookTask.IsCompleted); + await metricsHook.FinallyAsync(ctx, new Dictionary()); + this._meterProvider.ForceFlush(); // Assert metrics - Assert.NotEmpty(exportedItems); + Assert.NotEmpty(this._exportedItems); // check if the metric feature_flag.evaluation_success_total is present in the exported items - var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); + var metric = this._exportedItems.FirstOrDefault(m => m.Name == metricName); Assert.NotNull(metric); - var noOtherMetric = exportedItems.All(m => m.Name == metricName); + var noOtherMetric = this._exportedItems.All(m => m.Name == metricName); Assert.True(noOtherMetric); } [Fact] - public void Before_Test() + public async Task Before_Test() { - // Arrange metrics collector - var exportedItems = new List(); - Sdk.CreateMeterProviderBuilder() - .AddMeter("*") - .ConfigureResource(r => r.AddService("openfeature")) - .AddInMemoryExporter(exportedItems, - option => option.PeriodicExportingMetricReaderOptions = - new PeriodicExportingMetricReaderOptions { ExportIntervalMilliseconds = 100 }) - .Build(); - // Arrange const string metricName1 = "feature_flag.evaluation_active_count"; const string metricName2 = "feature_flag.evaluation_requests_total"; - var otelHook = new MetricsHook(); + var metricsHook = new MetricsHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var hookTask = otelHook.BeforeAsync(ctx, new Dictionary()); - // Wait for the metrics to be exported - Thread.Sleep(150); - - // Assert - Assert.True(hookTask.IsCompleted); + await metricsHook.BeforeAsync(ctx, new Dictionary()); + this._meterProvider.ForceFlush(); // Assert metrics - Assert.NotEmpty(exportedItems); + Assert.NotEmpty(this._exportedItems); // check if the metric is present in the exported items - var metric1 = exportedItems.FirstOrDefault(m => m.Name == metricName1); + var metric1 = this._exportedItems.FirstOrDefault(m => m.Name == metricName1); Assert.NotNull(metric1); - var metric2 = exportedItems.FirstOrDefault(m => m.Name == metricName2); + var metric2 = this._exportedItems.FirstOrDefault(m => m.Name == metricName2); Assert.NotNull(metric2); - var noOtherMetric = exportedItems.All(m => m.Name == metricName1 || m.Name == metricName2); + var noOtherMetric = this._exportedItems.All(m => m.Name == metricName1 || m.Name == metricName2); Assert.True(noOtherMetric); } } From 703f418e9f6464f73642c972a93ea4189994d00e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:17:15 +0000 Subject: [PATCH 09/17] Cleanup tests. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index f41c523d..7e2caee9 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -11,41 +12,56 @@ namespace OpenFeature.Tests.Hooks; -public class TracingHookTest +[CollectionDefinition(nameof(TracingHookTest), DisableParallelization = true)] +public class TracingHookTest : IDisposable { - [Fact] - public async Task TestAfter() + private readonly List _exportedItems; + private readonly TracerProvider _tracerProvider; + private readonly Tracer _tracer; + + public TracingHookTest() { - // Arrange // List that will be populated with the traces by InMemoryExporter - var exportedItems = new List(); + this._exportedItems = []; // Create a new in-memory exporter - var tracerProvider = Sdk.CreateTracerProviderBuilder() + this._tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") .ConfigureResource(r => r.AddService("in-memory-test")) - .AddInMemoryExporter(exportedItems) + .AddInMemoryExporter(this._exportedItems) .Build(); - var tracer = tracerProvider.GetTracer("my-tracer"); + this._tracer = this._tracerProvider.GetTracer("my-tracer"); + } + +#pragma warning disable CA1816 + public void Dispose() + { + this._tracerProvider.Shutdown(); + } +#pragma warning restore CA1816 + [Fact] + public async Task TestAfter() + { + // Arrange var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var span = tracer.StartActiveSpan("my-span"); + var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), new Dictionary()); span.End(); - tracerProvider.ForceFlush(); + this._tracerProvider.ForceFlush(); // Assert - Assert.Single(exportedItems); - var rootSpan = exportedItems.First(); + Assert.Single(this._exportedItems); + var rootSpan = this._exportedItems.First(); Assert.Single(rootSpan.Events); ActivityEvent ev = rootSpan.Events.First(); @@ -60,34 +76,22 @@ await tracingHook.AfterAsync(ctx, public async Task TestError() { // Arrange - // List that will be populated with the traces by InMemoryExporter - var exportedItems = new List(); - - // Create a new in-memory exporter - var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddSource("my-tracer") - .ConfigureResource(r => r.AddService("in-memory-test")) - .AddInMemoryExporter(exportedItems) - .Build(); - - var tracer = tracerProvider.GetTracer("my-tracer"); - var tracingHook = new TracingHook(); var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - var span = tracer.StartActiveSpan("my-span"); - await tracingHook.ErrorAsync(ctx, new System.Exception("unexpected error"), + var span = this._tracer.StartActiveSpan("my-span"); + await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), new Dictionary()); span.End(); - tracerProvider.ForceFlush(); + this._tracerProvider.ForceFlush(); // Assert - Assert.Single(exportedItems); - var rootSpan = exportedItems.First(); + Assert.Single(this._exportedItems); + var rootSpan = this._exportedItems.First(); Assert.Single(rootSpan.Events); var ev = rootSpan.Events.First(); From 729bd65f48903c0f42daf0bdd14aa177130410cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:18:42 +0000 Subject: [PATCH 10/17] Adding empty span. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../Hooks/TracingHookTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 7e2caee9..485bc180 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -72,6 +72,26 @@ await tracingHook.AfterAsync(ctx, Assert.Contains(new KeyValuePair("feature_flag.provider_name", "my-provider"), ev.Tags); } + [Fact] + public async Task TestAfter_NoSpan() + { + // Arrange + var tracingHook = new TracingHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + await tracingHook.AfterAsync(ctx, + new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), + new Dictionary()); + + this._tracerProvider.ForceFlush(); + + // Assert + Assert.Empty(this._exportedItems); + } + [Fact] public async Task TestError() { @@ -100,4 +120,23 @@ public async Task TestError() Assert.Contains(new KeyValuePair("exception.message", "unexpected error"), ev.Tags); } + + [Fact] + public async Task TestError_NoSpan() + { + // Arrange + var tracingHook = new TracingHook(); + var evaluationContext = EvaluationContext.Empty; + var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, + new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + + // Act + await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), + new Dictionary()); + + this._tracerProvider.ForceFlush(); + + // Assert + Assert.Empty(this._exportedItems); + } } From 7f5af18fcd56e36f04061709e34c60dc6039a810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:24:14 +0000 Subject: [PATCH 11/17] Adding .ConfigureAwait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- test/OpenFeature.Tests/Hooks/MetricsHookTests.cs | 8 ++++---- test/OpenFeature.Tests/Hooks/TracingHookTests.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index be6dd097..0d83eee2 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -50,7 +50,7 @@ public async Task After_Test() // Act await metricsHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()); + new Dictionary()).ConfigureAwait(true); this._meterProvider.ForceFlush(); // Assert metrics @@ -75,7 +75,7 @@ public async Task Error_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()); + await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true);; this._meterProvider.ForceFlush(); // Assert metrics @@ -100,7 +100,7 @@ public async Task Finally_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.FinallyAsync(ctx, new Dictionary()); + await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true);; this._meterProvider.ForceFlush(); // Assert metrics @@ -126,7 +126,7 @@ public async Task Before_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.BeforeAsync(ctx, new Dictionary()); + await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true);; this._meterProvider.ForceFlush(); // Assert metrics diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 485bc180..5dadca64 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -54,7 +54,7 @@ public async Task TestAfter() var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()); + new Dictionary()).ConfigureAwait(true);; span.End(); this._tracerProvider.ForceFlush(); @@ -84,7 +84,7 @@ public async Task TestAfter_NoSpan() // Act await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()); + new Dictionary()).ConfigureAwait(true);; this._tracerProvider.ForceFlush(); @@ -104,7 +104,7 @@ public async Task TestError() // Act var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()); + new Dictionary()).ConfigureAwait(true);; span.End(); this._tracerProvider.ForceFlush(); @@ -132,7 +132,7 @@ public async Task TestError_NoSpan() // Act await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()); + new Dictionary()).ConfigureAwait(true);; this._tracerProvider.ForceFlush(); From c0b6cc0bc4bf1f4698d724236c3d8c8d6efea315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:28:08 +0000 Subject: [PATCH 12/17] Fix dotnet format. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- test/OpenFeature.Tests/Hooks/MetricsHookTests.cs | 6 +++--- test/OpenFeature.Tests/Hooks/TracingHookTests.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index 0d83eee2..163348be 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -75,7 +75,7 @@ public async Task Error_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true);; + await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true); ; this._meterProvider.ForceFlush(); // Assert metrics @@ -100,7 +100,7 @@ public async Task Finally_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true);; + await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true); ; this._meterProvider.ForceFlush(); // Assert metrics @@ -126,7 +126,7 @@ public async Task Before_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true);; + await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true); ; this._meterProvider.ForceFlush(); // Assert metrics diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index 5dadca64..a1794b55 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -54,7 +54,7 @@ public async Task TestAfter() var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()).ConfigureAwait(true);; + new Dictionary()).ConfigureAwait(true); ; span.End(); this._tracerProvider.ForceFlush(); @@ -84,7 +84,7 @@ public async Task TestAfter_NoSpan() // Act await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()).ConfigureAwait(true);; + new Dictionary()).ConfigureAwait(true); ; this._tracerProvider.ForceFlush(); @@ -104,7 +104,7 @@ public async Task TestError() // Act var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()).ConfigureAwait(true);; + new Dictionary()).ConfigureAwait(true); ; span.End(); this._tracerProvider.ForceFlush(); @@ -132,7 +132,7 @@ public async Task TestError_NoSpan() // Act await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()).ConfigureAwait(true);; + new Dictionary()).ConfigureAwait(true); ; this._tracerProvider.ForceFlush(); From d0157aee475edd6a23b773323b39a26f90e790f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:31:08 +0000 Subject: [PATCH 13/17] Removed comma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- test/OpenFeature.Tests/Hooks/MetricsHookTests.cs | 6 +++--- test/OpenFeature.Tests/Hooks/TracingHookTests.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index 163348be..cd057f7e 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -75,7 +75,7 @@ public async Task Error_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true); ; + await metricsHook.ErrorAsync(ctx, new Exception(), new Dictionary()).ConfigureAwait(true); this._meterProvider.ForceFlush(); // Assert metrics @@ -100,7 +100,7 @@ public async Task Finally_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true); ; + await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true); this._meterProvider.ForceFlush(); // Assert metrics @@ -126,7 +126,7 @@ public async Task Before_Test() new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); // Act - await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true); ; + await metricsHook.BeforeAsync(ctx, new Dictionary()).ConfigureAwait(true); this._meterProvider.ForceFlush(); // Assert metrics diff --git a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs index a1794b55..0c236f40 100644 --- a/test/OpenFeature.Tests/Hooks/TracingHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/TracingHookTests.cs @@ -54,7 +54,7 @@ public async Task TestAfter() var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()).ConfigureAwait(true); ; + new Dictionary()).ConfigureAwait(true); span.End(); this._tracerProvider.ForceFlush(); @@ -84,7 +84,7 @@ public async Task TestAfter_NoSpan() // Act await tracingHook.AfterAsync(ctx, new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"), - new Dictionary()).ConfigureAwait(true); ; + new Dictionary()).ConfigureAwait(true); this._tracerProvider.ForceFlush(); @@ -104,7 +104,7 @@ public async Task TestError() // Act var span = this._tracer.StartActiveSpan("my-span"); await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()).ConfigureAwait(true); ; + new Dictionary()).ConfigureAwait(true); span.End(); this._tracerProvider.ForceFlush(); @@ -132,7 +132,7 @@ public async Task TestError_NoSpan() // Act await tracingHook.ErrorAsync(ctx, new Exception("unexpected error"), - new Dictionary()).ConfigureAwait(true); ; + new Dictionary()).ConfigureAwait(true); this._tracerProvider.ForceFlush(); From 902a1023ce489f96d3e286965fcf680fa7daa5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:38:10 +0000 Subject: [PATCH 14/17] Adding README information. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- README.md | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/README.md b/README.md index d9a277c2..2a6f1889 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,121 @@ services.AddOpenFeature() }); ``` +### Trace Hook + +For this hook to function correctly a global `TracerProvider` must be set, an example of how to do this can be found below. + +The `open telemetry hook` taps into the after and error methods of the hook lifecycle to write `events` and `attributes` to an existing `span`. +For this, an active span must be set in the `Tracer`, otherwise the hook will no-op. + +### Example + +The following example demonstrates the use of the `OpenTelemetry hook` with the `OpenFeature dotnet-sdk`. The traces are sent to a `jaeger` OTLP collector running at `localhost:4317`. + +```csharp +using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature.Hooks; +using OpenTelemetry.Exporter; +using OpenTelemetry.Resources; +using OpenTelemetry; +using OpenTelemetry.Trace; + +namespace OpenFeatureTestApp +{ + class Hello { + static void Main(string[] args) { + + // set up the OpenTelemetry OTLP exporter + var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddSource("my-tracer") + .ConfigureResource(r => r.AddService("jaeger-test")) + .AddOtlpExporter(o => + { + o.ExportProcessorType = ExportProcessorType.Simple; + }) + .Build(); + + // add the Otel Hook to the OpenFeature instance + OpenFeature.Api.Instance.AddHooks(new TracingHook()); + + var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); + + // Set the flagdProvider as the provider for the OpenFeature SDK + OpenFeature.Api.Instance.SetProvider(flagdProvider); + + var client = OpenFeature.Api.Instance.GetClient("my-app"); + + var val = client.GetBooleanValueAsync("myBoolFlag", false, null); + + // Print the value of the 'myBoolFlag' feature flag + System.Console.WriteLine(val.Result.ToString()); + } + } +} +``` + +After running this example, you will be able to see the traces, including the events sent by the hook in your Jaeger UI. + +### Metrics Hook + +For this hook to function correctly a global `MeterProvider` must be set. +`MetricsHook` performs metric collection by tapping into various hook stages. + +Below are the metrics extracted by this hook and dimensions they carry: + +| Metric key | Description | Unit | Dimensions | +| -------------------------------------- | ------------------------------- | ------------ | ----------------------------------- | +| feature_flag.evaluation_requests_total | Number of evaluation requests | {request} | key, provider name | +| feature_flag.evaluation_success_total | Flag evaluation successes | {impression} | key, provider name, reason, variant | +| feature_flag.evaluation_error_total | Flag evaluation errors | Counter | key, provider name | +| feature_flag.evaluation_active_count | Active flag evaluations counter | Counter | key | + +Consider the following code example for usage. + +### Example + +The following example demonstrates the use of the `OpenTelemetry hook` with the `OpenFeature dotnet-sdk`. The metrics are sent to the `console`. + +```csharp +using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature; +using OpenFeature.Hooks; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +namespace OpenFeatureTestApp +{ + class Hello { + static void Main(string[] args) { + + // set up the OpenTelemetry OTLP exporter + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("OpenFeature") + .ConfigureResource(r => r.AddService("openfeature-test")) + .AddConsoleExporter() + .Build(); + + // add the Otel Hook to the OpenFeature instance + OpenFeature.Api.Instance.AddHooks(new MetricsHook()); + + var flagdProvider = new FlagdProvider(new Uri("http://localhost:8013")); + + // Set the flagdProvider as the provider for the OpenFeature SDK + OpenFeature.Api.Instance.SetProvider(flagdProvider); + + var client = OpenFeature.Api.Instance.GetClient("my-app"); + + var val = client.GetBooleanValueAsync("myBoolFlag", false, null); + + // Print the value of the 'myBoolFlag' feature flag + System.Console.WriteLine(val.Result.ToString()); + } + } +} +``` + +After running this example, you should be able to see some metrics being generated into the console. + ## ⭐️ Support the project From 6a3d911684bb39c314fd52a3a3e7988bec0653b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:27:49 +0000 Subject: [PATCH 15/17] Using the new API. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Hooks/TracingHook.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature/Hooks/TracingHook.cs b/src/OpenFeature/Hooks/TracingHook.cs index 037a00c9..b3afdd46 100644 --- a/src/OpenFeature/Hooks/TracingHook.cs +++ b/src/OpenFeature/Hooks/TracingHook.cs @@ -35,7 +35,7 @@ public override ValueTask ErrorAsync(HookContext context, System.Exception { #if NET9_0_OR_GREATER // For dotnet9 we should use the new API https://learn.microsoft.com/en-gb/dotnet/api/system.diagnostics.activity.addexception?view=net-9.0 - // Activity.Current?.AddException(error); + Activity.Current?.AddException(error); #else var tagsCollection = new ActivityTagsCollection { From 52b47da9ae92ebae8eac55178165995cb6690afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:37:07 +0000 Subject: [PATCH 16/17] Fix subscribed Meter. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- test/OpenFeature.Tests/Hooks/MetricsHookTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index cd057f7e..69b935d6 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -22,7 +22,7 @@ public MetricsHookTest() // Arrange metrics collector this._exportedItems = []; this._meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter("*") + .AddMeter("OpenFeature") .ConfigureResource(r => r.AddService("open-feature")) .AddInMemoryExporter(this._exportedItems, option => option.PeriodicExportingMetricReaderOptions = From 41c915dc94fc0c59bb1fe2387209f8fdbb1dad68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 7 Jan 2025 08:49:04 +0000 Subject: [PATCH 17/17] Fix breaking change. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- src/OpenFeature/Hooks/MetricsHook.cs | 7 +++++-- test/OpenFeature.Tests/Hooks/MetricsHookTests.cs | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature/Hooks/MetricsHook.cs b/src/OpenFeature/Hooks/MetricsHook.cs index 99fe4c9b..b6d088a5 100644 --- a/src/OpenFeature/Hooks/MetricsHook.cs +++ b/src/OpenFeature/Hooks/MetricsHook.cs @@ -85,7 +85,10 @@ public override ValueTask ErrorAsync(HookContext context, Exception error, } /// - public override ValueTask FinallyAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + public override ValueTask FinallyAsync(HookContext context, + FlagEvaluationDetails evaluationDetails, + IReadOnlyDictionary? hints = null, + CancellationToken cancellationToken = default) { var tagList = new TagList { @@ -95,6 +98,6 @@ public override ValueTask FinallyAsync(HookContext context, IReadOnlyDicti this._evaluationActiveUpDownCounter.Add(-1, tagList); - return base.FinallyAsync(context, hints, cancellationToken); + return base.FinallyAsync(context, evaluationDetails, hints, cancellationToken); } } diff --git a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs index 69b935d6..5192df37 100644 --- a/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs +++ b/test/OpenFeature.Tests/Hooks/MetricsHookTests.cs @@ -98,9 +98,10 @@ public async Task Finally_Test() var evaluationContext = EvaluationContext.Empty; var ctx = new HookContext("my-flag", "foo", Constant.FlagValueType.String, new ClientMetadata("my-client", "1.0"), new Metadata("my-provider"), evaluationContext); + var evaluationDetails = new FlagEvaluationDetails("my-flag", "foo", Constant.ErrorType.None, "STATIC", "default"); // Act - await metricsHook.FinallyAsync(ctx, new Dictionary()).ConfigureAwait(true); + await metricsHook.FinallyAsync(ctx, evaluationDetails, new Dictionary()).ConfigureAwait(true); this._meterProvider.ForceFlush(); // Assert metrics