From 402b394f1c21d6cdbc8caab0d3164c97f0aa2e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:17:01 +0000 Subject: [PATCH 01/24] Adding inital 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> --- .vscode/settings.json | 3 ++ .../MetricsHook.cs | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..79fae382 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "DotnetSdkContrib.sln" +} diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs new file mode 100644 index 00000000..cd1ecba9 --- /dev/null +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Hooks.Otel +{ + public class MetricsHook : Hook + { + // support a functional means of adding custom attributes to metrics, as in the JS implementation, which allows consumers to provide a function that takes the flag metadata + // and returns OTel attributes + + + public override Task Before(HookContext context, IReadOnlyDictionary hints = null) + { + // evaluationActiveUpDownCounter + // evaluationRequestCounter + return base.Before(context, hints); + } + + public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) + { + // evaluationSuccessCounter + return base.After(context, details, hints); + } + + public override Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null) + { + // evaluationErrorCounter + return base.Error(context, error, hints); + } + + public override Task Finally(HookContext context, IReadOnlyDictionary hints = null) + { + // evaluationActiveUpDownCounter + return base.Finally(context, hints); + } + } +} + From 9d56d1b2125aa7c6f359cc8ff56e225472ae6a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:42:42 +0000 Subject: [PATCH 02/24] Add metrics tracking to MetricsHook class 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> --- .../MetricsHook.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index cd1ecba9..b0b4859d 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Threading.Tasks; using OpenFeature.Model; @@ -7,8 +9,24 @@ namespace OpenFeature.Contrib.Hooks.Otel { public class MetricsHook : Hook { + private const string FEATURE_FLAG = "feature_flag"; + private const string EXCEPTION_ATTR = "exception"; + + private const string ACTIVE_COUNT_NAME = FEATURE_FLAG + ".evaluation_active_count"; + private const string REQUESTS_TOTAL_NAME = FEATURE_FLAG + ".evaluation_requests_total"; + private const string SUCCESS_TOTAL_NAME = FEATURE_FLAG + ".evaluation_success_total"; + private const string ERROR_TOTAL_NAME = FEATURE_FLAG + ".evaluation_error_total"; + + private const string KEY_ATTR = FEATURE_FLAG + ".key"; + private const string PROVIDER_NAME_ATTR = FEATURE_FLAG + ".provider_name"; + private const string VARIANT_ATTR = FEATURE_FLAG + ".variant"; + private const string REASON_ATTR = FEATURE_FLAG + ".reason"; + // support a functional means of adding custom attributes to metrics, as in the JS implementation, which allows consumers to provide a function that takes the flag metadata // and returns OTel attributes + public MetricsHook() + { + } public override Task Before(HookContext context, IReadOnlyDictionary hints = null) From 06ba9d42ffbe3fc4621ed035a0e480ed05703df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:58:08 +0000 Subject: [PATCH 03/24] Add metrics instrumentation to MetricsHook class 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> --- .../MetricsHook.cs | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index b0b4859d..4843555c 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Reflection; using System.Threading.Tasks; using OpenFeature.Model; @@ -9,30 +10,49 @@ namespace OpenFeature.Contrib.Hooks.Otel { public class MetricsHook : Hook { - private const string FEATURE_FLAG = "feature_flag"; + private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName(); + private static readonly string InstrumentationName = AssemblyName.Name; + private static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + private const string EXCEPTION_ATTR = "exception"; - private const string ACTIVE_COUNT_NAME = FEATURE_FLAG + ".evaluation_active_count"; - private const string REQUESTS_TOTAL_NAME = FEATURE_FLAG + ".evaluation_requests_total"; - private const string SUCCESS_TOTAL_NAME = FEATURE_FLAG + ".evaluation_success_total"; - private const string ERROR_TOTAL_NAME = FEATURE_FLAG + ".evaluation_error_total"; + private const string ACTIVE_COUNT_NAME = "feature_flag.evaluation_active_count"; + private const string REQUESTS_TOTAL_NAME = "feature_flag.evaluation_requests_total"; + private const string SUCCESS_TOTAL_NAME = "feature_flag.evaluation_success_total"; + private const string ERROR_TOTAL_NAME = "feature_flag.evaluation_error_total"; + + private const string KEY_ATTR = "feature_flag.key"; + private const string PROVIDER_NAME_ATTR = "feature_flag.provider_name"; + private const string VARIANT_ATTR = "feature_flag.variant"; + private const string REASON_ATTR = "feature_flag.reason"; - private const string KEY_ATTR = FEATURE_FLAG + ".key"; - private const string PROVIDER_NAME_ATTR = FEATURE_FLAG + ".provider_name"; - private const string VARIANT_ATTR = FEATURE_FLAG + ".variant"; - private const string REASON_ATTR = FEATURE_FLAG + ".reason"; + private readonly UpDownCounter _evaluationActiveUpDownCounter; + private readonly Counter _evaluationRequestCounter; + private readonly Counter _evaluationSuccessCounter; + private readonly Counter _evaluationErrorCounter; - // support a functional means of adding custom attributes to metrics, as in the JS implementation, which allows consumers to provide a function that takes the flag metadata - // and returns OTel attributes public MetricsHook() { + var meter = new Meter(InstrumentationName, InstrumentationVersion); + + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME); + _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME); + _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME); + _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME); } public override Task Before(HookContext context, IReadOnlyDictionary hints = null) { - // evaluationActiveUpDownCounter - // evaluationRequestCounter + var tagList = new TagList + { + { KEY_ATTR, context.FlagKey }, + { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name } + }; + + _evaluationActiveUpDownCounter.Add(1, tagList); + _evaluationRequestCounter.Add(1, tagList); + return base.Before(context, hints); } From 231ab4dde5c52ce8a53b2437b2b550e6818bba0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:00:03 +0000 Subject: [PATCH 04/24] Add descriptions to metric counters 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 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 4843555c..283516ba 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -21,6 +21,11 @@ public class MetricsHook : Hook private const string SUCCESS_TOTAL_NAME = "feature_flag.evaluation_success_total"; private const string ERROR_TOTAL_NAME = "feature_flag.evaluation_error_total"; + private const string ACTIVE_DESCRIPTION = "active flag evaluations counter"; + private const string REQUESTS_DESCRIPTION = "feature flag evaluation request counter"; + private const string SUCCESS_DESCRIPTION = "feature flag evaluation success counter"; + private const string ERROR_DESCRIPTION = "feature flag evaluation error counter"; + private const string KEY_ATTR = "feature_flag.key"; private const string PROVIDER_NAME_ATTR = "feature_flag.provider_name"; private const string VARIANT_ATTR = "feature_flag.variant"; @@ -35,10 +40,10 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME); - _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME); - _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME); - _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME, "double", ACTIVE_DESCRIPTION); + _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME, "double", REQUESTS_DESCRIPTION); + _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME, "double", SUCCESS_DESCRIPTION); + _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME, "double", ERROR_DESCRIPTION); } From c50235f40150fe199560265e19252ceb808827c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:01:45 +0000 Subject: [PATCH 05/24] Update MetricsHook.cs with new constant UNIT 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, 6 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 283516ba..4dc3a014 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -15,6 +15,7 @@ public class MetricsHook : Hook private static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); private const string EXCEPTION_ATTR = "exception"; + private const string UNIT = "double"; private const string ACTIVE_COUNT_NAME = "feature_flag.evaluation_active_count"; private const string REQUESTS_TOTAL_NAME = "feature_flag.evaluation_requests_total"; @@ -40,10 +41,10 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME, "double", ACTIVE_DESCRIPTION); - _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME, "double", REQUESTS_DESCRIPTION); - _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME, "double", SUCCESS_DESCRIPTION); - _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME, "double", ERROR_DESCRIPTION); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME, UNIT, ACTIVE_DESCRIPTION); + _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME, UNIT, REQUESTS_DESCRIPTION); + _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME, UNIT, SUCCESS_DESCRIPTION); + _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME, UNIT, ERROR_DESCRIPTION); } @@ -63,6 +64,7 @@ public override Task Before(HookContext context, IReadO public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) { + // evaluationSuccessCounter return base.After(context, details, hints); } From 53d9b0b66201203edd51eef9c111f7c2ac80fe3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:04:18 +0000 Subject: [PATCH 06/24] Add evaluationSuccessCounter metric tracking 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, 9 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 4dc3a014..51e4cca6 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -64,8 +64,16 @@ public override Task Before(HookContext context, IReadO public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) { + var tagList = new TagList + { + { KEY_ATTR, context.FlagKey }, + { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, + { VARIANT_ATTR, details.Variant ?? details.Value?.ToString() }, + { REASON_ATTR, details.Reason ?? "UNKNOWN" } + }; + + _evaluationSuccessCounter.Add(1, tagList); - // evaluationSuccessCounter return base.After(context, details, hints); } From 922fcec1940f5b3807601160aff0573b0580fc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:06:14 +0000 Subject: [PATCH 07/24] Add error tracking for evaluation errors 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, 9 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 51e4cca6..30db39b7 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -79,7 +79,15 @@ public override Task After(HookContext context, FlagEvaluationDetails d public override Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null) { - // evaluationErrorCounter + var tagList = new TagList + { + { KEY_ATTR, context.FlagKey }, + { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, + { EXCEPTION_ATTR, error?.Message ?? "Unknown error" } + }; + + _evaluationErrorCounter.Add(1, tagList); + return base.Error(context, error, hints); } From 98f69ebb35af88480d07b93d945cf6b9049fa9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:06:49 +0000 Subject: [PATCH 08/24] Add tags to evaluationActiveUpDownCounter in Finally method 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 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 30db39b7..d0d3c1f5 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -93,7 +93,14 @@ public override Task Error(HookContext context, Exception error, IReadOnly public override Task Finally(HookContext context, IReadOnlyDictionary hints = null) { - // evaluationActiveUpDownCounter + var tagList = new TagList + { + { KEY_ATTR, context.FlagKey }, + { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name } + }; + + _evaluationActiveUpDownCounter.Add(-1, tagList); + return base.Finally(context, hints); } } From 5010a93bf5ad3a93e4d84b3d4a00fd2c71f37de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:10:10 +0000 Subject: [PATCH 09/24] Remove empty line in MetricsHook.cs 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 | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index d0d3c1f5..cd171b55 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -47,7 +47,6 @@ public MetricsHook() _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME, UNIT, ERROR_DESCRIPTION); } - public override Task Before(HookContext context, IReadOnlyDictionary hints = null) { var tagList = new TagList @@ -105,4 +104,3 @@ public override Task Finally(HookContext context, IReadOnlyDictionary Date: Thu, 14 Dec 2023 15:14:19 +0000 Subject: [PATCH 10/24] Refactor metrics hook class and use constants for metric names and descriptions 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> --- .../MetricsConstants.cs | 23 +++++++++ .../MetricsHook.cs | 48 ++++++------------- 2 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs new file mode 100644 index 00000000..45883bb2 --- /dev/null +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs @@ -0,0 +1,23 @@ +namespace OpenFeature.Contrib.Hooks.Otel +{ + internal static class MetricsConstants + { + internal const string UNIT = "double"; + + internal const string ACTIVE_COUNT_NAME = "feature_flag.evaluation_active_count"; + internal const string REQUESTS_TOTAL_NAME = "feature_flag.evaluation_requests_total"; + internal const string SUCCESS_TOTAL_NAME = "feature_flag.evaluation_success_total"; + internal const string ERROR_TOTAL_NAME = "feature_flag.evaluation_error_total"; + + internal const string ACTIVE_DESCRIPTION = "active flag evaluations counter"; + internal const string REQUESTS_DESCRIPTION = "feature flag evaluation request counter"; + internal const string SUCCESS_DESCRIPTION = "feature flag evaluation success counter"; + internal const string ERROR_DESCRIPTION = "feature flag evaluation error counter"; + + internal const string KEY_ATTR = "feature_flag.key"; + internal const string PROVIDER_NAME_ATTR = "feature_flag.provider_name"; + internal const string VARIANT_ATTR = "feature_flag.variant"; + internal const string REASON_ATTR = "feature_flag.reason"; + internal const string EXCEPTION_ATTR = "exception"; + } +} diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index cd171b55..5e5c8c31 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -14,24 +14,6 @@ public class MetricsHook : Hook private static readonly string InstrumentationName = AssemblyName.Name; private static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); - private const string EXCEPTION_ATTR = "exception"; - private const string UNIT = "double"; - - private const string ACTIVE_COUNT_NAME = "feature_flag.evaluation_active_count"; - private const string REQUESTS_TOTAL_NAME = "feature_flag.evaluation_requests_total"; - private const string SUCCESS_TOTAL_NAME = "feature_flag.evaluation_success_total"; - private const string ERROR_TOTAL_NAME = "feature_flag.evaluation_error_total"; - - private const string ACTIVE_DESCRIPTION = "active flag evaluations counter"; - private const string REQUESTS_DESCRIPTION = "feature flag evaluation request counter"; - private const string SUCCESS_DESCRIPTION = "feature flag evaluation success counter"; - private const string ERROR_DESCRIPTION = "feature flag evaluation error counter"; - - private const string KEY_ATTR = "feature_flag.key"; - private const string PROVIDER_NAME_ATTR = "feature_flag.provider_name"; - private const string VARIANT_ATTR = "feature_flag.variant"; - private const string REASON_ATTR = "feature_flag.reason"; - private readonly UpDownCounter _evaluationActiveUpDownCounter; private readonly Counter _evaluationRequestCounter; private readonly Counter _evaluationSuccessCounter; @@ -41,18 +23,18 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(ACTIVE_COUNT_NAME, UNIT, ACTIVE_DESCRIPTION); - _evaluationRequestCounter = meter.CreateCounter(REQUESTS_TOTAL_NAME, UNIT, REQUESTS_DESCRIPTION); - _evaluationSuccessCounter = meter.CreateCounter(SUCCESS_TOTAL_NAME, UNIT, SUCCESS_DESCRIPTION); - _evaluationErrorCounter = meter.CreateCounter(ERROR_TOTAL_NAME, UNIT, ERROR_DESCRIPTION); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ACTIVE_COUNT_NAME, MetricsConstants.UNIT, MetricsConstants.ACTIVE_DESCRIPTION); + _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.REQUESTS_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.REQUESTS_DESCRIPTION); + _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SUCCESS_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.SUCCESS_DESCRIPTION); + _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ERROR_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.ERROR_DESCRIPTION); } public override Task Before(HookContext context, IReadOnlyDictionary hints = null) { var tagList = new TagList { - { KEY_ATTR, context.FlagKey }, - { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name } + { MetricsConstants.KEY_ATTR, context.FlagKey }, + { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name } }; _evaluationActiveUpDownCounter.Add(1, tagList); @@ -65,10 +47,10 @@ public override Task After(HookContext context, FlagEvaluationDetails d { var tagList = new TagList { - { KEY_ATTR, context.FlagKey }, - { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, - { VARIANT_ATTR, details.Variant ?? details.Value?.ToString() }, - { REASON_ATTR, details.Reason ?? "UNKNOWN" } + { MetricsConstants.KEY_ATTR, context.FlagKey }, + { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, + { MetricsConstants.VARIANT_ATTR, details.Variant ?? details.Value?.ToString() }, + { MetricsConstants.REASON_ATTR, details.Reason ?? "UNKNOWN" } }; _evaluationSuccessCounter.Add(1, tagList); @@ -80,9 +62,9 @@ public override Task Error(HookContext context, Exception error, IReadOnly { var tagList = new TagList { - { KEY_ATTR, context.FlagKey }, - { PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, - { EXCEPTION_ATTR, error?.Message ?? "Unknown error" } + { MetricsConstants.KEY_ATTR, context.FlagKey }, + { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, + { MetricsConstants.EXCEPTION_ATTR, error?.Message ?? "Unknown error" } }; _evaluationErrorCounter.Add(1, tagList); @@ -94,8 +76,8 @@ public override Task Finally(HookContext context, IReadOnlyDictionary Date: Thu, 14 Dec 2023 15:25:16 +0000 Subject: [PATCH 11/24] Add 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> --- .../MetricsHook.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 5e5c8c31..aafdfdbc 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -8,6 +8,9 @@ namespace OpenFeature.Contrib.Hooks.Otel { + /// + /// Represents a hook for capturing metrics related to flag evaluations. + /// public class MetricsHook : Hook { private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName(); @@ -19,6 +22,9 @@ public class MetricsHook : Hook private readonly Counter _evaluationSuccessCounter; private readonly Counter _evaluationErrorCounter; + /// + /// Initializes a new instance of the class. + /// public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); @@ -29,6 +35,16 @@ public MetricsHook() _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ERROR_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.ERROR_DESCRIPTION); } + /// + /// Executes before the flag evaluation and captures metrics related to the evaluation. + /// The metrics are captured in the following order: + /// 1. The active count is incremented. (feature_flag.evaluation_active_count) + /// 2. The request count is incremented. (feature_flag.evaluation_requests_total) + /// + /// The type of the flag value. + /// The hook context. + /// The optional hints. + /// The evaluation context. public override Task Before(HookContext context, IReadOnlyDictionary hints = null) { var tagList = new TagList @@ -43,6 +59,17 @@ public override Task Before(HookContext context, IReadO return base.Before(context, hints); } + + /// + /// Executes after the flag evaluation and captures metrics related to the evaluation. + /// The metrics are captured in the following order: + /// 1. The success count is incremented. (feature_flag.evaluation_success_total) + /// + /// The type of the flag value. + /// The hook context. + /// The flag evaluation details. + /// The optional hints. + /// The evaluation context. public override Task After(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary hints = null) { var tagList = new TagList @@ -58,6 +85,16 @@ public override Task After(HookContext context, FlagEvaluationDetails d return base.After(context, details, hints); } + /// + /// Executes when an error occurs during flag evaluation and captures metrics related to the error. + /// The metrics are captured in the following order: + /// 1. The error count is incremented. (feature_flag.evaluation_error_total) + /// + /// The type of the flag value. + /// The hook context. + /// The exception that occurred. + /// The optional hints. + /// The evaluation context. public override Task Error(HookContext context, Exception error, IReadOnlyDictionary hints = null) { var tagList = new TagList @@ -72,6 +109,14 @@ public override Task Error(HookContext context, Exception error, IReadOnly return base.Error(context, error, hints); } + /// + /// Executes after the flag evaluation is complete and captures metrics related to the evaluation. + /// The active count is decremented. (feature_flag.evaluation_active_count) + /// + /// The type of the flag value. + /// The hook context. + /// The optional hints. + /// The evaluation context. public override Task Finally(HookContext context, IReadOnlyDictionary hints = null) { var tagList = new TagList From 5a0ca25d10dd0cc5fc9727258722301d7f3aa138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:39:35 +0000 Subject: [PATCH 12/24] Refactor metrics constants and attributes 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> --- .../MetricsConstants.cs | 28 ++++++++--------- .../MetricsHook.cs | 30 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs index 45883bb2..ac08edac 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs @@ -2,22 +2,22 @@ namespace OpenFeature.Contrib.Hooks.Otel { internal static class MetricsConstants { - internal const string UNIT = "double"; + internal const string Unit = "double"; - internal const string ACTIVE_COUNT_NAME = "feature_flag.evaluation_active_count"; - internal const string REQUESTS_TOTAL_NAME = "feature_flag.evaluation_requests_total"; - internal const string SUCCESS_TOTAL_NAME = "feature_flag.evaluation_success_total"; - internal const string ERROR_TOTAL_NAME = "feature_flag.evaluation_error_total"; + 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 ACTIVE_DESCRIPTION = "active flag evaluations counter"; - internal const string REQUESTS_DESCRIPTION = "feature flag evaluation request counter"; - internal const string SUCCESS_DESCRIPTION = "feature flag evaluation success counter"; - internal const string ERROR_DESCRIPTION = "feature flag evaluation error counter"; + 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 KEY_ATTR = "feature_flag.key"; - internal const string PROVIDER_NAME_ATTR = "feature_flag.provider_name"; - internal const string VARIANT_ATTR = "feature_flag.variant"; - internal const string REASON_ATTR = "feature_flag.reason"; - internal const string EXCEPTION_ATTR = "exception"; + internal const string KeyAttr = "feature_flag.key"; + internal const string ProviderNameAttr = "feature_flag.provider_name"; + internal const string VariantAttr = "feature_flag.variant"; + internal const string ReasonAttr = "feature_flag.reason"; + internal const string ExceptionAttr = "exception"; } } diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index aafdfdbc..400d5b27 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -29,10 +29,10 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ACTIVE_COUNT_NAME, MetricsConstants.UNIT, MetricsConstants.ACTIVE_DESCRIPTION); - _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.REQUESTS_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.REQUESTS_DESCRIPTION); - _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SUCCESS_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.SUCCESS_DESCRIPTION); - _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ERROR_TOTAL_NAME, MetricsConstants.UNIT, MetricsConstants.ERROR_DESCRIPTION); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, MetricsConstants.Unit, MetricsConstants.ActiveDescription); + _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, MetricsConstants.Unit, MetricsConstants.RequestsDescription); + _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, MetricsConstants.Unit, MetricsConstants.SuccessDescription); + _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, MetricsConstants.Unit, MetricsConstants.ErrorDescription); } /// @@ -49,8 +49,8 @@ public override Task Before(HookContext context, IReadO { var tagList = new TagList { - { MetricsConstants.KEY_ATTR, context.FlagKey }, - { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name } + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name } }; _evaluationActiveUpDownCounter.Add(1, tagList); @@ -74,10 +74,10 @@ public override Task After(HookContext context, FlagEvaluationDetails d { var tagList = new TagList { - { MetricsConstants.KEY_ATTR, context.FlagKey }, - { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, - { MetricsConstants.VARIANT_ATTR, details.Variant ?? details.Value?.ToString() }, - { MetricsConstants.REASON_ATTR, details.Reason ?? "UNKNOWN" } + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, + { MetricsConstants.VariantAttr, details.Variant ?? details.Value?.ToString() }, + { MetricsConstants.ReasonAttr, details.Reason ?? "UNKNOWN" } }; _evaluationSuccessCounter.Add(1, tagList); @@ -99,9 +99,9 @@ public override Task Error(HookContext context, Exception error, IReadOnly { var tagList = new TagList { - { MetricsConstants.KEY_ATTR, context.FlagKey }, - { MetricsConstants.PROVIDER_NAME_ATTR, context.ProviderMetadata.Name }, - { MetricsConstants.EXCEPTION_ATTR, error?.Message ?? "Unknown error" } + { MetricsConstants.KeyAttr, context.FlagKey }, + { MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }, + { MetricsConstants.ExceptionAttr, error?.Message ?? "Unknown error" } }; _evaluationErrorCounter.Add(1, tagList); @@ -121,8 +121,8 @@ public override Task Finally(HookContext context, IReadOnlyDictionary Date: Thu, 14 Dec 2023 16:05:43 +0000 Subject: [PATCH 13/24] Update metrics constants names 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/MetricsConstants.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs index ac08edac..f5a47506 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs @@ -14,10 +14,10 @@ internal static class MetricsConstants internal const string SuccessDescription = "feature flag evaluation success counter"; internal const string ErrorDescription = "feature flag evaluation error counter"; - internal const string KeyAttr = "feature_flag.key"; - internal const string ProviderNameAttr = "feature_flag.provider_name"; - internal const string VariantAttr = "feature_flag.variant"; - internal const string ReasonAttr = "feature_flag.reason"; + 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"; } } From e96b2a7a93fa0ad6a7fc745c915542270b7667b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:07:16 +0000 Subject: [PATCH 14/24] Add meter name to MetricsHook class 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 400d5b27..cc5d922f 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -10,6 +10,7 @@ namespace OpenFeature.Contrib.Hooks.Otel { /// /// Represents a hook for capturing metrics related to flag evaluations. + /// The meter name is "OpenFeature.Contrib.Hooks.Otel". /// public class MetricsHook : Hook { From 3a1edefddb05296206a725f5b13b5318157bc525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:15:20 +0000 Subject: [PATCH 15/24] Add MetricsHookTest class for OpenFeature.Contrib.Hooks.Otel.Test 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> --- .../MetricsHookTest.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs new file mode 100644 index 00000000..4fbc4120 --- /dev/null +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using OpenFeature.Model; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using Xunit; + +namespace OpenFeature.Contrib.Hooks.Otel.Test +{ + 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.After(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 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 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.Error(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 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); + } + + } + + +} From 64e9e61398ef71a2e9f24bd0c5c76296d30de806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:17:21 +0000 Subject: [PATCH 16/24] Add Finally_Test to MetricsHookTest.cs 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> --- .../MetricsHookTest.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs index 4fbc4120..0658bc24 100644 --- a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs @@ -86,7 +86,40 @@ public void Error_Test() 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.Finally(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); + } + } } From fcaea2859d9c4475026f51cc845e5f3849bbe1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:19:56 +0000 Subject: [PATCH 17/24] Refactor 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> --- .../MetricsHookTest.cs | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs index 0658bc24..7fc9ee0f 100644 --- a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Threading; using OpenFeature.Model; using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using Xunit; @@ -42,7 +40,7 @@ public void After_Test() // Assert metrics Assert.NotEmpty(exportedItems); - // check if the metric feature_flag.evaluation_success_total is present in the exported items + // check if the metric is present in the exported items var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); Assert.NotNull(metric); @@ -78,7 +76,7 @@ public void Error_Test() // Assert metrics Assert.NotEmpty(exportedItems); - // check if the metric feature_flag.evaluation_success_total is present in the exported items + // check if the metric is present in the exported items var metric = exportedItems.FirstOrDefault(m => m.Name == metricName); Assert.NotNull(metric); @@ -121,5 +119,45 @@ public void Finally_Test() 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.Before(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); + } } } From a813239e463d776ede95391b0f345b92af30cd7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:22:11 +0000 Subject: [PATCH 18/24] Refactor metrics assertions in MetricsHookTest.cs 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> --- .../MetricsHookTest.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs index 7fc9ee0f..ffc83b2c 100644 --- a/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/MetricsHookTest.cs @@ -36,14 +36,14 @@ public void After_Test() // 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); } @@ -72,18 +72,18 @@ public void Error_Test() // 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() { @@ -108,14 +108,14 @@ public void Finally_Test() // 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); } @@ -145,17 +145,17 @@ public void Before_Test() // 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); } From 67eb243c23fd799af8db17316af0b04e3f592660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:37:40 +0000 Subject: [PATCH 19/24] Update README.md with usage examples for Traces and Metrics 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 | 53 +++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/README.md b/src/OpenFeature.Contrib.Hooks.Otel/README.md index 77fcfc88..bea69357 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/README.md +++ b/src/OpenFeature.Contrib.Hooks.Otel/README.md @@ -4,7 +4,7 @@ - open-feature/dotnet-sdk >= v1.0 -## Usage +## Usage - Traces For this hook to function correctly a global `TracerProvider` must be set, an example of how to do this can be found below. @@ -12,6 +12,7 @@ The `open telemetry hook` taps into the after and error methods of the hook life 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 @@ -31,7 +32,7 @@ namespace OpenFeatureTestApp var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("my-tracer") .ConfigureResource(r => r.AddService("jaeger-test")) - .AddOtlpExporter(o => + .AddOtlpExporter(o => { o.ExportProcessorType = ExportProcessorType.Simple; }) @@ -64,6 +65,54 @@ In case something went wrong during a feature flag evaluation, you will see an e ![](./assets/otlp-error.png) +## Usage - Metrics + +For this hook to function correctly a global `MeterProvider` must be set, an example of how to do this can be found below. + +### Example + +The following example demonstrates the use of the `OpenTelemetry hook` with the `OpenFeature dotnet-sdk`. The metrics are sent to a `prometheus` OTLP collector running at `localhost:4317`. + +```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 + 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.GetBooleanValue("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. + ## License Apache 2.0 - See [LICENSE](./../../LICENSE) for more information. From 4cb73a58a5bd7eee25fc5dd0c04b2082ac5c06d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:39:34 +0000 Subject: [PATCH 20/24] Update OpenTelemetry hook example to send metrics to the console 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/README.md b/src/OpenFeature.Contrib.Hooks.Otel/README.md index bea69357..4b7e92b3 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/README.md +++ b/src/OpenFeature.Contrib.Hooks.Otel/README.md @@ -71,7 +71,7 @@ For this hook to function correctly a global `MeterProvider` must be set, an exa ### Example -The following example demonstrates the use of the `OpenTelemetry hook` with the `OpenFeature dotnet-sdk`. The metrics are sent to a `prometheus` OTLP collector running at `localhost:4317`. +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; From f8e52e6d3631b87b936b8dc56534ec41d189477f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:38:10 +0000 Subject: [PATCH 21/24] Update MetricsConstants and MetricsHook to use long instead of double 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> --- .../MetricsConstants.cs | 2 +- .../MetricsHook.cs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs index f5a47506..d15d8d99 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs @@ -2,7 +2,7 @@ namespace OpenFeature.Contrib.Hooks.Otel { internal static class MetricsConstants { - internal const string Unit = "double"; + internal const string Unit = "long"; internal const string ActiveCountName = "feature_flag.evaluation_active_count"; internal const string RequestsTotalName = "feature_flag.evaluation_requests_total"; diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index cc5d922f..2b384562 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -18,10 +18,10 @@ public class MetricsHook : Hook private static readonly string InstrumentationName = AssemblyName.Name; private static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); - private readonly UpDownCounter _evaluationActiveUpDownCounter; - private readonly Counter _evaluationRequestCounter; - private readonly Counter _evaluationSuccessCounter; - private readonly Counter _evaluationErrorCounter; + private readonly UpDownCounter _evaluationActiveUpDownCounter; + private readonly Counter _evaluationRequestCounter; + private readonly Counter _evaluationSuccessCounter; + private readonly Counter _evaluationErrorCounter; /// /// Initializes a new instance of the class. @@ -30,10 +30,10 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, MetricsConstants.Unit, MetricsConstants.ActiveDescription); - _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, MetricsConstants.Unit, MetricsConstants.RequestsDescription); - _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, MetricsConstants.Unit, MetricsConstants.SuccessDescription); - _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, MetricsConstants.Unit, MetricsConstants.ErrorDescription); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, MetricsConstants.Unit, MetricsConstants.ActiveDescription); + _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, MetricsConstants.Unit, MetricsConstants.RequestsDescription); + _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, MetricsConstants.Unit, MetricsConstants.SuccessDescription); + _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, MetricsConstants.Unit, MetricsConstants.ErrorDescription); } /// From 24d1682d9985fa71b5a84f9598dd082039854965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:56:20 +0000 Subject: [PATCH 22/24] Remove unused constant and update metric counter parameters 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/MetricsConstants.cs | 2 -- src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs index d15d8d99..dcbffe99 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs @@ -2,8 +2,6 @@ namespace OpenFeature.Contrib.Hooks.Otel { internal static class MetricsConstants { - internal const string Unit = "long"; - 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"; diff --git a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs index 2b384562..f5fd56f9 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs +++ b/src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs @@ -16,7 +16,7 @@ public class MetricsHook : Hook { private static readonly AssemblyName AssemblyName = typeof(MetricsHook).Assembly.GetName(); private static readonly string InstrumentationName = AssemblyName.Name; - private static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + private static readonly string InstrumentationVersion = AssemblyName.Version?.ToString(); private readonly UpDownCounter _evaluationActiveUpDownCounter; private readonly Counter _evaluationRequestCounter; @@ -30,10 +30,10 @@ public MetricsHook() { var meter = new Meter(InstrumentationName, InstrumentationVersion); - _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, MetricsConstants.Unit, MetricsConstants.ActiveDescription); - _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, MetricsConstants.Unit, MetricsConstants.RequestsDescription); - _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, MetricsConstants.Unit, MetricsConstants.SuccessDescription); - _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, MetricsConstants.Unit, MetricsConstants.ErrorDescription); + _evaluationActiveUpDownCounter = meter.CreateUpDownCounter(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription); + _evaluationRequestCounter = meter.CreateCounter(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription); + _evaluationSuccessCounter = meter.CreateCounter(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription); + _evaluationErrorCounter = meter.CreateCounter(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription); } /// From a46278db64f784e97016ab16abd098e405cecf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:02:20 +0000 Subject: [PATCH 23/24] Update metrics documentation in README.md 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Hooks.Otel/README.md b/src/OpenFeature.Contrib.Hooks.Otel/README.md index 4b7e92b3..744a00c4 100644 --- a/src/OpenFeature.Contrib.Hooks.Otel/README.md +++ b/src/OpenFeature.Contrib.Hooks.Otel/README.md @@ -67,7 +67,19 @@ In case something went wrong during a feature flag evaluation, you will see an e ## Usage - Metrics -For this hook to function correctly a global `MeterProvider` must be set, an example of how to do this can be found below. +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 From ea63383725a904bf246f57b47a3e46f35adec9a1 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Mon, 18 Dec 2023 12:19:37 -0500 Subject: [PATCH 24/24] fixup: add askpt to component_owners Signed-off-by: Todd Baert --- .github/component_owners.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/component_owners.yml b/.github/component_owners.yml index db652298..3bb488e5 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -4,6 +4,7 @@ components: src/OpenFeature.Contrib.Hooks.Otel: - bacherfl - toddbaert + - askpt src/OpenFeature.Contrib.Providers.Flagd: - bacherfl - toddbaert @@ -17,6 +18,7 @@ components: test/OpenFeature.Contrib.Hooks.Otel.Test: - bacherfl - toddbaert + - askpt test/OpenFeature.Contrib.Providers.Flagd.Test: - bacherfl - toddbaert @@ -27,4 +29,4 @@ components: - matthewelwell ignored-authors: - - renovate-bot \ No newline at end of file + - renovate-bot