Skip to content

Commit

Permalink
feat: Add Metrics Hook (#114)
Browse files Browse the repository at this point in the history
Signed-off-by: André Silva <[email protected]>
  • Loading branch information
askpt authored Dec 19, 2023
1 parent 755c549 commit 5845e2b
Show file tree
Hide file tree
Showing 6 changed files with 386 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ components:
src/OpenFeature.Contrib.Hooks.Otel:
- bacherfl
- toddbaert
- askpt
src/OpenFeature.Contrib.Providers.Flagd:
- bacherfl
- toddbaert
Expand All @@ -17,6 +18,7 @@ components:
test/OpenFeature.Contrib.Hooks.Otel.Test:
- bacherfl
- toddbaert
- askpt
test/OpenFeature.Contrib.Providers.Flagd.Test:
- bacherfl
- toddbaert
Expand All @@ -27,4 +29,4 @@ components:
- matthewelwell

ignored-authors:
- renovate-bot
- renovate-bot
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "DotnetSdkContrib.sln"
}
21 changes: 21 additions & 0 deletions src/OpenFeature.Contrib.Hooks.Otel/MetricsConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace OpenFeature.Contrib.Hooks.Otel
{
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";
}
}
134 changes: 134 additions & 0 deletions src/OpenFeature.Contrib.Hooks.Otel/MetricsHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Reflection;
using System.Threading.Tasks;
using OpenFeature.Model;

namespace OpenFeature.Contrib.Hooks.Otel
{
/// <summary>
/// Represents a hook for capturing metrics related to flag evaluations.
/// The meter name is "OpenFeature.Contrib.Hooks.Otel".
/// </summary>
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 readonly UpDownCounter<long> _evaluationActiveUpDownCounter;
private readonly Counter<long> _evaluationRequestCounter;
private readonly Counter<long> _evaluationSuccessCounter;
private readonly Counter<long> _evaluationErrorCounter;

/// <summary>
/// Initializes a new instance of the <see cref="MetricsHook"/> class.
/// </summary>
public MetricsHook()
{
var meter = new Meter(InstrumentationName, InstrumentationVersion);

_evaluationActiveUpDownCounter = meter.CreateUpDownCounter<long>(MetricsConstants.ActiveCountName, description: MetricsConstants.ActiveDescription);
_evaluationRequestCounter = meter.CreateCounter<long>(MetricsConstants.RequestsTotalName, "{request}", MetricsConstants.RequestsDescription);
_evaluationSuccessCounter = meter.CreateCounter<long>(MetricsConstants.SuccessTotalName, "{impression}", MetricsConstants.SuccessDescription);
_evaluationErrorCounter = meter.CreateCounter<long>(MetricsConstants.ErrorTotalName, description: MetricsConstants.ErrorDescription);
}

/// <summary>
/// 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)
/// </summary>
/// <typeparam name="T">The type of the flag value.</typeparam>
/// <param name="context">The hook context.</param>
/// <param name="hints">The optional hints.</param>
/// <returns>The evaluation context.</returns>
public override Task<EvaluationContext> Before<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
{
var tagList = new TagList
{
{ MetricsConstants.KeyAttr, context.FlagKey },
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }
};

_evaluationActiveUpDownCounter.Add(1, tagList);
_evaluationRequestCounter.Add(1, tagList);

return base.Before(context, hints);
}


/// <summary>
/// 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)
/// </summary>
/// <typeparam name="T">The type of the flag value.</typeparam>
/// <param name="context">The hook context.</param>
/// <param name="details">The flag evaluation details.</param>
/// <param name="hints">The optional hints.</param>
/// <returns>The evaluation context.</returns>
public override Task After<T>(HookContext<T> context, FlagEvaluationDetails<T> details, IReadOnlyDictionary<string, object> hints = null)
{
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" }
};

_evaluationSuccessCounter.Add(1, tagList);

return base.After(context, details, hints);
}

/// <summary>
/// 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)
/// </summary>
/// <typeparam name="T">The type of the flag value.</typeparam>
/// <param name="context">The hook context.</param>
/// <param name="error">The exception that occurred.</param>
/// <param name="hints">The optional hints.</param>
/// <returns>The evaluation context.</returns>
public override Task Error<T>(HookContext<T> context, Exception error, IReadOnlyDictionary<string, object> hints = null)
{
var tagList = new TagList
{
{ MetricsConstants.KeyAttr, context.FlagKey },
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name },
{ MetricsConstants.ExceptionAttr, error?.Message ?? "Unknown error" }
};

_evaluationErrorCounter.Add(1, tagList);

return base.Error(context, error, hints);
}

/// <summary>
/// Executes after the flag evaluation is complete and captures metrics related to the evaluation.
/// The active count is decremented. (feature_flag.evaluation_active_count)
/// </summary>
/// <typeparam name="T">The type of the flag value.</typeparam>
/// <param name="context">The hook context.</param>
/// <param name="hints">The optional hints.</param>
/// <returns>The evaluation context.</returns>
public override Task Finally<T>(HookContext<T> context, IReadOnlyDictionary<string, object> hints = null)
{
var tagList = new TagList
{
{ MetricsConstants.KeyAttr, context.FlagKey },
{ MetricsConstants.ProviderNameAttr, context.ProviderMetadata.Name }
};

_evaluationActiveUpDownCounter.Add(-1, tagList);

return base.Finally(context, hints);
}
}
}
64 changes: 62 additions & 2 deletions src/OpenFeature.Contrib.Hooks.Otel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -32,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;
})
Expand Down Expand Up @@ -65,6 +65,66 @@ 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.
`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.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.
Loading

0 comments on commit 5845e2b

Please sign in to comment.