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