diff --git a/src/OpenFeature/Hooks/LoggingHook.cs b/src/OpenFeature/Hooks/LoggingHook.cs new file mode 100644 index 00000000..0a78e5fe --- /dev/null +++ b/src/OpenFeature/Hooks/LoggingHook.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using OpenFeature.Model; + +namespace OpenFeature.Hooks +{ + /// + /// The logging hook is a hook which logs messages during the flag evaluation life-cycle. + /// + public sealed partial class LoggingHook : Hook + { + private readonly ILogger _logger; + private readonly bool _includeContext; + + /// + /// Initialise a with a and optional Evaluation Context. will + /// include properties in the to the generated logs. + /// + public LoggingHook(ILogger logger, bool includeContext = false) + { + this._logger = logger; + this._includeContext = includeContext; + } + + /// + public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var evaluationContext = this._includeContext ? context.EvaluationContext : null; + + var beforeLogContent = new LoggingHookContent( + context.ClientMetadata.Name, + context.ProviderMetadata.Name, + context.FlagKey, + context.DefaultValue?.ToString(), + evaluationContext); + + this.HookBeforeStageExecuted(beforeLogContent); + + return base.BeforeAsync(context, hints, cancellationToken); + } + + /// + public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var evaluationContext = this._includeContext ? context.EvaluationContext : null; + + var beforeLogContent = new LoggingHookContent( + context.ClientMetadata.Name, + context.ProviderMetadata.Name, + context.FlagKey, + context.DefaultValue?.ToString(), + evaluationContext); + + this.HookErrorStageExecuted(beforeLogContent); + + return base.ErrorAsync(context, error, hints, cancellationToken); + } + + /// + public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) + { + var evaluationContext = this._includeContext ? context.EvaluationContext : null; + + var beforeLogContent = new LoggingHookContent( + context.ClientMetadata.Name, + context.ProviderMetadata.Name, + context.FlagKey, + context.DefaultValue?.ToString(), + evaluationContext); + + this.HookAfterStageExecuted(beforeLogContent); + + return base.AfterAsync(context, details, hints, cancellationToken); + } + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "Before Flag Evaluation {Content}")] + partial void HookBeforeStageExecuted(LoggingHookContent content); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Error, + Message = "Error during Flag Evaluation {Content}")] + partial void HookErrorStageExecuted(LoggingHookContent content); + + [LoggerMessage( + EventId = 0, + Level = LogLevel.Debug, + Message = "After Flag Evaluation {Content}")] + partial void HookAfterStageExecuted(LoggingHookContent content); + + /// + /// Generates a log string with contents provided by the . + /// + /// Specification for log contents found at https://github.com/open-feature/spec/blob/d261f68331b94fd8ed10bc72bc0485cfc72a51a8/specification/appendix-a-included-utilities.md#logging-hook + /// + /// + internal class LoggingHookContent + { + private readonly string _domain; + private readonly string _providerName; + private readonly string _flagKey; + private readonly string _defaultValue; + private readonly EvaluationContext? _evaluationContext; + + public LoggingHookContent(string? domain, string? providerName, string flagKey, string? defaultValue, EvaluationContext? evaluationContext = null) + { + this._domain = string.IsNullOrEmpty(domain) ? "missing" : domain!; + this._providerName = string.IsNullOrEmpty(providerName) ? "missing" : providerName!; + this._flagKey = flagKey; + this._defaultValue = string.IsNullOrEmpty(defaultValue) ? "missing" : defaultValue!; + this._evaluationContext = evaluationContext; + } + + public override string ToString() + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("Domain:"); + stringBuilder.Append(this._domain); + stringBuilder.Append(Environment.NewLine); + + stringBuilder.Append("ProviderName:"); + stringBuilder.Append(this._providerName); + stringBuilder.Append(Environment.NewLine); + + stringBuilder.Append("FlagKey:"); + stringBuilder.Append(this._flagKey); + stringBuilder.Append(Environment.NewLine); + + stringBuilder.Append("DefaultValue:"); + stringBuilder.Append(this._defaultValue); + stringBuilder.Append(Environment.NewLine); + + if (this._evaluationContext != null) + { + stringBuilder.Append("Context:"); + stringBuilder.Append(Environment.NewLine); + foreach (var kvp in this._evaluationContext.AsDictionary()) + { + stringBuilder.Append('\t'); + stringBuilder.Append(kvp.Key); + stringBuilder.Append(':'); + stringBuilder.Append(GetValueString(kvp.Value) ?? "missing"); + stringBuilder.Append(Environment.NewLine); + } + } + + return stringBuilder.ToString(); + } + + static string? GetValueString(Value value) + { + if (value.IsNull) + return string.Empty; + + if (value.IsString) + return value.AsString; + + if (value.IsBoolean) + return value.AsBoolean.ToString(); + + if (value.IsDateTime) + return value.AsDateTime?.ToString("O"); + + return value.ToString(); + } + } + } +} diff --git a/src/OpenFeature/LoggingHook.cs b/src/OpenFeature/LoggingHook.cs deleted file mode 100644 index 726944bc..00000000 --- a/src/OpenFeature/LoggingHook.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using OpenFeature.Model; - -namespace OpenFeature -{ - /// - /// - /// - public sealed partial class LoggingHook : Hook - { - private readonly ILogger _logger; - private readonly bool _includeContext; - - /// - /// - /// - public LoggingHook(ILogger logger, bool includeContext = false) - { - this._logger = logger; - this._includeContext = includeContext; - } - - /// - public override ValueTask BeforeAsync(HookContext context, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - if (this._includeContext) - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString(), - context.EvaluationContext); - - this.HookBeforeStageExecuted(beforeLogContent); - } - else - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString()); - - this.HookBeforeStageExecuted(beforeLogContent); - } - - return base.BeforeAsync(context, hints, cancellationToken); - } - - /// - public override ValueTask ErrorAsync(HookContext context, Exception error, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - if (this._includeContext) - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString(), - context.EvaluationContext); - - this.HookErrorStageExecuted(beforeLogContent); - } - else - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString()); - - this.HookErrorStageExecuted(beforeLogContent); - } - - return base.ErrorAsync(context, error, hints, cancellationToken); - } - - /// - public override ValueTask AfterAsync(HookContext context, FlagEvaluationDetails details, IReadOnlyDictionary? hints = null, CancellationToken cancellationToken = default) - { - if (this._includeContext) - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString(), - context.EvaluationContext); - - this.HookAfterStageExecuted(beforeLogContent); - } - else - { - var beforeLogContent = new LoggingHookContent( - context.ClientMetadata.Name, - context.ProviderMetadata.Name, - context.FlagKey, - context.DefaultValue?.ToString()); - - this.HookAfterStageExecuted(beforeLogContent); - } - - return base.AfterAsync(context, details, hints, cancellationToken); - } - - [LoggerMessage( - EventId = 0, - Level = LogLevel.Debug, - Message = "Before Flag Evaluation {Content}")] - partial void HookBeforeStageExecuted(LoggingHookContent content); - - [LoggerMessage( - EventId = 0, - Level = LogLevel.Error, - Message = "Error during Flag Evaluation {Content}")] - partial void HookErrorStageExecuted(LoggingHookContent content); - - [LoggerMessage( - EventId = 0, - Level = LogLevel.Debug, - Message = "After Flag Evaluation {Content}")] - partial void HookAfterStageExecuted(LoggingHookContent content); - - internal class LoggingHookContent - { - private readonly string _domain; - private readonly string _providerName; - private readonly string _flagKey; - private readonly string _defaultValue; - private readonly EvaluationContext? _evaluationContext; - - private string? _cachedToString; - - public LoggingHookContent(string? domain, string? providerName, string flagKey, string? defaultValue, EvaluationContext? evaluationContext = null) - { - this._domain = string.IsNullOrEmpty(domain) ? "missing" : domain!; - this._providerName = string.IsNullOrEmpty(providerName) ? "missing" : providerName!; - this._flagKey = flagKey; - this._defaultValue = string.IsNullOrEmpty(defaultValue) ? "missing" : defaultValue!; - this._evaluationContext = evaluationContext; - } - - public override string ToString() - { - if (this._cachedToString == null) - { - var stringBuilder = new StringBuilder(); - - stringBuilder.Append("Domain:"); - stringBuilder.Append(this._domain); - stringBuilder.Append(Environment.NewLine); - - stringBuilder.Append("ProviderName:"); - stringBuilder.Append(this._providerName); - stringBuilder.Append(Environment.NewLine); - - stringBuilder.Append("FlagKey:"); - stringBuilder.Append(this._flagKey); - stringBuilder.Append(Environment.NewLine); - - stringBuilder.Append("DefaultValue:"); - stringBuilder.Append(this._defaultValue); - stringBuilder.Append(Environment.NewLine); - - if (this._evaluationContext != null) - { - stringBuilder.Append("Context:"); - stringBuilder.Append(Environment.NewLine); - foreach (var kvp in this._evaluationContext.AsDictionary()) - { - stringBuilder.Append('\t'); - stringBuilder.Append(kvp.Key); - stringBuilder.Append(':'); - stringBuilder.Append(GetValueString(kvp.Value) ?? "missing"); - stringBuilder.Append(Environment.NewLine); - } - } - - this._cachedToString = stringBuilder.ToString(); - } - - return this._cachedToString; - } - - static string? GetValueString(Value value) - { - if (value.IsNull) - return string.Empty; - - if (value.IsString) - return value.AsString; - - if (value.IsBoolean) - return value.AsBoolean.ToString(); - - if (value.IsDateTime) - return value.AsDateTime?.ToString("O"); - - return value.ToString(); - } - } - } -}