From 4de9a9a10c1685f14f5035b8ea669cbd694204cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:55:46 +0000 Subject: [PATCH 1/8] Adding class for MetricHookCustomDimensions 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> --- .../MetricHookCustomDimensions.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs new file mode 100644 index 00000000..593186e5 --- /dev/null +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; + +namespace OpenFeature.Contrib.Hooks.Otel +{ + /// + /// Represents a custom dimension list for a metric hook. + /// + public class MetricHookCustomDimensions + { + private readonly TagList _keyValuePairs = new TagList(); + + /// + /// Adds a custom dimension to the list. + /// + /// The key of the custom dimension. + /// The value of the custom dimension. + /// The custom dimension list. + public MetricHookCustomDimensions Add(string key, string value) + { + _keyValuePairs.Add(key, value); + return this; + } + + internal TagList GetTagList() + { + return _keyValuePairs; + } + } +} \ No newline at end of file From e700a536a24153b4e8c5ff72de96a6a1f31bbcab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:59:22 +0000 Subject: [PATCH 2/8] Add support for custom dimensions in MetricsHook 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.Contrib.Hooks.Otel/MetricsHook.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index f5fd56f9..f9e77e8e 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -22,12 +22,16 @@ public class MetricsHook : Hook private readonly Counter _evaluationRequestCounter; private readonly Counter _evaluationSuccessCounter; private readonly Counter _evaluationErrorCounter; + private readonly TagList _customDimensionsTagList; /// /// Initializes a new instance of the class. /// - public MetricsHook() + /// The optional custom dimensions. + public MetricsHook(MetricHookCustomDimensions customDimensions = null) { + _customDimensionsTagList = customDimensions?.GetTagList() ?? new TagList(); + var meter = new Meter(InstrumentationName, InstrumentationVersion); _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription); From e0be24ef2c37d3ace5d19b04f6c77890a4c9f0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:06:46 +0000 Subject: [PATCH 3/8] Refactor MetricHookCustomDimensions to use List instead of TagList 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> --- .../MetricHookCustomDimensions.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs index 593186e5..70493a4c 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Diagnostics; namespace OpenFeature.Contrib.Hooks.Otel @@ -7,7 +9,7 @@ namespace OpenFeature.Contrib.Hooks.Otel /// public class MetricHookCustomDimensions { - private readonly TagList _keyValuePairs = new TagList(); + private readonly List> _keyValuePairs = new List>(); /// /// Adds a custom dimension to the list. @@ -15,15 +17,15 @@ public class MetricHookCustomDimensions /// The key of the custom dimension. /// The value of the custom dimension. /// The custom dimension list. - public MetricHookCustomDimensions Add(string key, string value) + public MetricHookCustomDimensions Add(string key, object value) { - _keyValuePairs.Add(key, value); + _keyValuePairs.Add(new KeyValuePair(key, value)); return this; } - internal TagList GetTagList() + internal KeyValuePair[] GetTagList() { - return _keyValuePairs; + return _keyValuePairs.ToArray(); } } } \ No newline at end of file From 674b984af66c9c3743780160b679129e23811d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:08:28 +0000 Subject: [PATCH 4/8] Update MetricsHook to use KeyValuePair array for custom dimensions 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.Contrib.Hooks.Otel/MetricsHook.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index f9e77e8e..1f512660 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -22,7 +22,7 @@ public class MetricsHook : Hook private readonly Counter _evaluationRequestCounter; private readonly Counter _evaluationSuccessCounter; private readonly Counter _evaluationErrorCounter; - private readonly TagList _customDimensionsTagList; + private readonly KeyValuePair[] _customDimensionsTagList; /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ public class MetricsHook : Hook /// The optional custom dimensions. public MetricsHook(MetricHookCustomDimensions customDimensions = null) { - _customDimensionsTagList = customDimensions?.GetTagList() ?? new TagList(); + _customDimensionsTagList = customDimensions?.GetTagList() ?? new KeyValuePair[] { }; var meter = new Meter(InstrumentationName, InstrumentationVersion); @@ -77,7 +77,7 @@ public override Task Before(HookContext context, IReadO /// The evaluation context. public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) { - var tagList = new TagList + var tagList = new TagList(_customDimensionsTagList) { { MetricsConstants.KeyAttr, context.FlagKey }, { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, From 145002210d0067ed8e98d73b8078b69e48d49764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 22 Dec 2023 08:20:46 +0000 Subject: [PATCH 5/8] Remove unused using statements and update MetricsHook constructor 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> --- .../MetricHookCustomDimensions.cs | 2 -- src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs index 70493a4c..6e30a71b 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricHookCustomDimensions.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; namespace OpenFeature.Contrib.Hooks.Otel { diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 1f512660..52ebb8c9 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -30,7 +30,7 @@ public class MetricsHook : Hook /// The optional custom dimensions. public MetricsHook(MetricHookCustomDimensions customDimensions = null) { - _customDimensionsTagList = customDimensions?.GetTagList() ?? new KeyValuePair[] { }; + _customDimensionsTagList = customDimensions?.GetTagList() ?? Array.Empty>(); var meter = new Meter(InstrumentationName, InstrumentationVersion); From 3edc4623311d4884c93daa2ed03602bef496e529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:03:46 +0000 Subject: [PATCH 6/8] Adding documentation. 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.Contrib.Hooks.Otel/README.md | 60 ++++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/README.md b/src/OpenFeature.Contrib.Hooks.Otel/README.md index c15b5806..d8f91d43 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/README.md +++ b/src/OpenFeature.Contrib.Hooks.Otel/README.md @@ -72,12 +72,12 @@ For this hook to function correctly a global `MeterProvider` must be set. 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 | +| 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, custom dimensions\* | +| 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. @@ -125,6 +125,54 @@ namespace OpenFeatureTestApp After running this example, you should be able to see some metrics being generated into the console. +### Custom dimensions + +The metrics hook can be enriched with custom dimensions. This is only available for the `feature_flag.evaluation_success_total` metric. +In order to use it, you need to create a instance of the `MetricsHook` class with a dictionary of custom dimensions. + +See example: + +```csharp +using OpenFeature.Contrib.Providers.Flagd; +using OpenFeature; +using OpenFeature.Contrib.Hooks.Otel; +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.Contrib.Hooks.Otel") + .ConfigureResource(r => r.AddService("openfeature-test")) + .AddConsoleExporter() + .Build(); + + // add the Otel Hook to the OpenFeature instance + var options = new MetricHookCustomDimensions() + .Add("key", "value") + .Add("test", "test"); + OpenFeature.Api.Instance.AddHooks(new MetricsHook(options)); + + 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.GetBooleanValue("myBoolFlag", false, null); + + // Print the value of the 'myBoolFlag' feature flag + System.Console.WriteLine(val.Result.ToString()); + } + } +} +``` + ## License Apache 2.0 - See [LICENSE](./../../LICENSE) for more information. From a62f7e8f1ffdad93f05e9a2540a823b7b496b34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:12:37 +0000 Subject: [PATCH 7/8] Added unit 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> --- .../OpenFeature.Contrib.Hooks.Otel.csproj | 4 ++ .../MetricHookCustomDimensionsTest.cs | 38 +++++++++++++++++++ ...OpenFeature.Contrib.Hooks.Otel.Test.csproj | 5 +-- 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 test/OpenFeature.Contrib.Hooks.Otel.Test/MetricHookCustomDimensionsTest.cs diff --git a/src/OpenFeature.Contrib.Hooks.Otel/OpenFeature.Contrib.Hooks.Otel.csproj b/src/OpenFeature.Contrib.Hooks.Otel/OpenFeature.Contrib.Hooks.Otel.csproj index a4f54a75..005adbb9 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/OpenFeature.Contrib.Hooks.Otel.csproj +++ b/src/OpenFeature.Contrib.Hooks.Otel/OpenFeature.Contrib.Hooks.Otel.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricHookCustomDimensionsTest.cs b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricHookCustomDimensionsTest.cs new file mode 100644 index 00000000..a3242f9a --- /dev/null +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricHookCustomDimensionsTest.cs @@ -0,0 +1,38 @@ +using Xunit; + +namespace OpenFeature.Contrib.Hooks.Otel.Test +{ + public class MetricHookCustomDimensionsTest + { + [Fact] + public void Adds_CustomDimension_HasValues() + { + // Arrange + var customDimensions = new MetricHookCustomDimensions(); + string key = "dimensionKey"; + object value = "dimensionValue"; + + // Act + customDimensions.Add(key, value); + + // Assert + var tagList = customDimensions.GetTagList(); + Assert.Single(tagList); + Assert.Equal(key, tagList[0].Key); + Assert.Equal(value, tagList[0].Value); + } + + [Fact] + public void CustomDimensionToList_IsEmpty() + { + // Arrange + var customDimensions = new MetricHookCustomDimensions(); + + // Act + + // Assert + var tagList = customDimensions.GetTagList(); + Assert.Empty(tagList); + } + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj b/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj index c396ebec..8cad51e0 100644 --- a/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj @@ -4,9 +4,8 @@ - - - + + From 37f8b44d9051dc5eadbbd5565309fa833ca9ae53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:20:18 +0000 Subject: [PATCH 8/8] Initial attempt for custom function. 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.Contrib.Hooks.Otel/MetricsHook.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 52ebb8c9..466defa9 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -22,16 +22,15 @@ public class MetricsHook : Hook private readonly Counter _evaluationRequestCounter; private readonly Counter _evaluationSuccessCounter; private readonly Counter _evaluationErrorCounter; - private readonly KeyValuePair[] _customDimensionsTagList; + private readonly Func, KeyValuePair[]> _customDimensions; /// /// Initializes a new instance of the class. /// /// The optional custom dimensions. - public MetricsHook(MetricHookCustomDimensions customDimensions = null) + public MetricsHook(Func, KeyValuePair[]> customDimensions = null) { - _customDimensionsTagList = customDimensions?.GetTagList() ?? Array.Empty>(); - + _customDimensions = customDimensions; var meter = new Meter(InstrumentationName, InstrumentationVersion); _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription); @@ -77,7 +76,8 @@ public override Task Before(HookContext context, IReadO /// The evaluation context. public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) { - var tagList = new TagList(_customDimensionsTagList) + var initalTagList = _customDimensions != null ? _customDimensions(details) : Array.Empty>(); + var tagList = new TagList(initalTagList) { { MetricsConstants.KeyAttr, context.FlagKey }, { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name },