diff --git a/src/Polly.Core/CompositeStrategyBuilderBase.cs b/src/Polly.Core/CompositeStrategyBuilderBase.cs index 768ff29a062..a2017cc5eba 100644 --- a/src/Polly.Core/CompositeStrategyBuilderBase.cs +++ b/src/Polly.Core/CompositeStrategyBuilderBase.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; +using Polly.Telemetry; namespace Polly; @@ -27,7 +28,6 @@ private protected CompositeStrategyBuilderBase(CompositeStrategyBuilderBase othe Name = other.Name; Properties = other.Properties; TimeProvider = other.TimeProvider; - OnCreatingStrategy = other.OnCreatingStrategy; Randomizer = other.Randomizer; DiagnosticSource = other.DiagnosticSource; } @@ -75,18 +75,6 @@ private protected CompositeStrategyBuilderBase(CompositeStrategyBuilderBase othe [Required] internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; - /// - /// Gets or sets the callback that is invoked just before the final resilience strategy is being created. - /// - /// - /// This property is used by the telemetry infrastructure and should not be used directly by user code. - /// - /// - /// The default value is . - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public Action>? OnCreatingStrategy { get; set; } - /// /// Gets or sets the that is used by Polly to report resilience events. /// @@ -143,19 +131,16 @@ internal ResilienceStrategy BuildStrategy() _used = true; var strategies = _entries.Select(CreateResilienceStrategy).ToList(); - OnCreatingStrategy?.Invoke(strategies); if (strategies.Count == 0) { return NullResilienceStrategy.Instance; } - if (strategies.Count == 1) - { - return strategies[0]; - } - - return CompositeResilienceStrategy.Create(strategies); + return CompositeResilienceStrategy.Create( + strategies, + TelemetryUtil.CreateTelemetry(DiagnosticSource, Name, InstanceName, Properties, null), + TimeProvider); } private ResilienceStrategy CreateResilienceStrategy(Entry entry) diff --git a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs index f8c9cc90efe..b384d3ecb9d 100644 --- a/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs +++ b/src/Polly.Core/Hedging/HedgingResilienceStrategy.cs @@ -97,7 +97,7 @@ private async ValueTask> ExecuteCoreAsync( await HandleOnHedgingAsync( context, Outcome.FromResult(default), - new OnHedgingArguments(attempt, hasOutcome: false, executionTime: delay)).ConfigureAwait(context.ContinueOnCapturedContext); + new OnHedgingArguments(attempt, hasOutcome: false, duration: delay)).ConfigureAwait(context.ContinueOnCapturedContext); continue; } diff --git a/src/Polly.Core/Hedging/OnHedgingArguments.cs b/src/Polly.Core/Hedging/OnHedgingArguments.cs index 2b2db37b08e..04ab9d94045 100644 --- a/src/Polly.Core/Hedging/OnHedgingArguments.cs +++ b/src/Polly.Core/Hedging/OnHedgingArguments.cs @@ -10,12 +10,12 @@ public sealed class OnHedgingArguments /// /// The zero-based hedging attempt number. /// Indicates whether outcome is available. - /// The execution time of hedging attempt or the hedging delay in case the attempt was not finished in time. - public OnHedgingArguments(int attemptNumber, bool hasOutcome, TimeSpan executionTime) + /// The execution duration of hedging attempt or the hedging delay in case the attempt was not finished in time. + public OnHedgingArguments(int attemptNumber, bool hasOutcome, TimeSpan duration) { AttemptNumber = attemptNumber; HasOutcome = hasOutcome; - ExecutionTime = executionTime; + Duration = duration; } /// @@ -32,7 +32,7 @@ public OnHedgingArguments(int attemptNumber, bool hasOutcome, TimeSpan execution public bool HasOutcome { get; } /// - /// Gets the execution time of hedging attempt or the hedging delay in case the attempt was not finished in time. + /// Gets the execution duration of hedging attempt or the hedging delay in case the attempt was not finished in time. /// - public TimeSpan ExecutionTime { get; } + public TimeSpan Duration { get; } } diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 8da2c7a3d08..8e29facb138 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -88,8 +88,6 @@ Polly.CompositeStrategyBuilderBase.InstanceName.get -> string? Polly.CompositeStrategyBuilderBase.InstanceName.set -> void Polly.CompositeStrategyBuilderBase.Name.get -> string? Polly.CompositeStrategyBuilderBase.Name.set -> void -Polly.CompositeStrategyBuilderBase.OnCreatingStrategy.get -> System.Action!>? -Polly.CompositeStrategyBuilderBase.OnCreatingStrategy.set -> void Polly.CompositeStrategyBuilderBase.Properties.get -> Polly.ResilienceProperties! Polly.CompositeStrategyBuilderBase.Randomizer.get -> System.Func! Polly.CompositeStrategyBuilderBase.Randomizer.set -> void @@ -142,9 +140,9 @@ Polly.Hedging.HedgingStrategyOptions.ShouldHandle.get -> System.Func.ShouldHandle.set -> void Polly.Hedging.OnHedgingArguments Polly.Hedging.OnHedgingArguments.AttemptNumber.get -> int -Polly.Hedging.OnHedgingArguments.ExecutionTime.get -> System.TimeSpan +Polly.Hedging.OnHedgingArguments.Duration.get -> System.TimeSpan Polly.Hedging.OnHedgingArguments.HasOutcome.get -> bool -Polly.Hedging.OnHedgingArguments.OnHedgingArguments(int attemptNumber, bool hasOutcome, System.TimeSpan executionTime) -> void +Polly.Hedging.OnHedgingArguments.OnHedgingArguments(int attemptNumber, bool hasOutcome, System.TimeSpan duration) -> void Polly.HedgingCompositeStrategyBuilderExtensions Polly.LegacySupport Polly.NonReactiveResilienceStrategy @@ -329,9 +327,14 @@ Polly.StrategyBuilderContext.StrategyName.get -> string? Polly.StrategyBuilderContext.Telemetry.get -> Polly.Telemetry.ResilienceStrategyTelemetry! Polly.Telemetry.ExecutionAttemptArguments Polly.Telemetry.ExecutionAttemptArguments.AttemptNumber.get -> int -Polly.Telemetry.ExecutionAttemptArguments.ExecutionAttemptArguments(int attemptNumber, System.TimeSpan executionTime, bool handled) -> void -Polly.Telemetry.ExecutionAttemptArguments.ExecutionTime.get -> System.TimeSpan +Polly.Telemetry.ExecutionAttemptArguments.Duration.get -> System.TimeSpan +Polly.Telemetry.ExecutionAttemptArguments.ExecutionAttemptArguments(int attemptNumber, System.TimeSpan duration, bool handled) -> void Polly.Telemetry.ExecutionAttemptArguments.Handled.get -> bool +Polly.Telemetry.PipelineExecutedArguments +Polly.Telemetry.PipelineExecutedArguments.Duration.get -> System.TimeSpan +Polly.Telemetry.PipelineExecutedArguments.PipelineExecutedArguments(System.TimeSpan duration) -> void +Polly.Telemetry.PipelineExecutingArguments +Polly.Telemetry.PipelineExecutingArguments.PipelineExecutingArguments() -> void Polly.Telemetry.ResilienceEvent Polly.Telemetry.ResilienceEvent.EventName.get -> string! Polly.Telemetry.ResilienceEvent.ResilienceEvent() -> void diff --git a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs index 07d1ad22c25..752fd7f277e 100644 --- a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs +++ b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs @@ -4,16 +4,16 @@ public partial class ExecutionAttemptArguments { private static readonly ObjectPool Pool = new(() => new ExecutionAttemptArguments(), args => { - args.ExecutionTime = TimeSpan.Zero; + args.Duration = TimeSpan.Zero; args.AttemptNumber = 0; args.Handled = false; }); - internal static ExecutionAttemptArguments Get(int attempt, TimeSpan executionTime, bool handled) + internal static ExecutionAttemptArguments Get(int attempt, TimeSpan duration, bool handled) { var args = Pool.Get(); args.AttemptNumber = attempt; - args.ExecutionTime = executionTime; + args.Duration = duration; args.Handled = handled; return args; } diff --git a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs index a1376f7af3a..2126e1fa302 100644 --- a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs +++ b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs @@ -3,18 +3,18 @@ /// /// Arguments that encapsulate the execution attempt for retries or hedging. /// -public partial class ExecutionAttemptArguments +public sealed partial class ExecutionAttemptArguments { /// /// Initializes a new instance of the class. /// /// The execution attempt number. - /// The execution time. + /// The execution duration. /// Determines whether the attempt was handled by the strategy. - public ExecutionAttemptArguments(int attemptNumber, TimeSpan executionTime, bool handled) + public ExecutionAttemptArguments(int attemptNumber, TimeSpan duration, bool handled) { AttemptNumber = attemptNumber; - ExecutionTime = executionTime; + Duration = duration; Handled = handled; } @@ -28,9 +28,9 @@ private ExecutionAttemptArguments() public int AttemptNumber { get; private set; } /// - /// Gets the execution time of the attempt. + /// Gets the execution duration of the attempt. /// - public TimeSpan ExecutionTime { get; private set; } + public TimeSpan Duration { get; private set; } /// /// Gets a value indicating whether the outcome was handled by retry or hedging strategy. diff --git a/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs b/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs new file mode 100644 index 00000000000..e71018369dc --- /dev/null +++ b/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs @@ -0,0 +1,18 @@ +namespace Polly.Telemetry; + +public sealed partial class PipelineExecutedArguments +{ + private static readonly ObjectPool Pool = new(() => new PipelineExecutedArguments(), args => + { + args.Duration = TimeSpan.Zero; + }); + + internal static PipelineExecutedArguments Get(TimeSpan duration) + { + var args = Pool.Get(); + args.Duration = duration; + return args; + } + + internal static void Return(PipelineExecutedArguments args) => Pool.Return(args); +} diff --git a/src/Polly.Core/Telemetry/PipelineExecutedArguments.cs b/src/Polly.Core/Telemetry/PipelineExecutedArguments.cs new file mode 100644 index 00000000000..a49c8b504fd --- /dev/null +++ b/src/Polly.Core/Telemetry/PipelineExecutedArguments.cs @@ -0,0 +1,22 @@ +namespace Polly.Telemetry; + +/// +/// Arguments that indicate the pipeline execution started. +/// +public sealed partial class PipelineExecutedArguments +{ + /// + /// Initializes a new instance of the class. + /// + /// The pipeline execution duration. + public PipelineExecutedArguments(TimeSpan duration) => Duration = duration; + + internal PipelineExecutedArguments() + { + } + + /// + /// Gets the pipeline execution duration. + /// + public TimeSpan Duration { get; internal set; } +} diff --git a/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs b/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs new file mode 100644 index 00000000000..de6f4cc83fa --- /dev/null +++ b/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs @@ -0,0 +1,9 @@ +namespace Polly.Telemetry; + +/// +/// Arguments that indicate the pipeline execution started. +/// +public sealed class PipelineExecutingArguments +{ + internal static readonly PipelineExecutingArguments Instance = new(); +} diff --git a/src/Polly.Core/Telemetry/TelemetryUtil.cs b/src/Polly.Core/Telemetry/TelemetryUtil.cs index 07c7d02bd86..af44bed2d8e 100644 --- a/src/Polly.Core/Telemetry/TelemetryUtil.cs +++ b/src/Polly.Core/Telemetry/TelemetryUtil.cs @@ -6,6 +6,10 @@ internal static class TelemetryUtil internal const string ExecutionAttempt = "ExecutionAttempt"; + internal const string PipelineExecuting = "PipelineExecuting"; + + internal const string PipelineExecuted = "PipelineExecuted"; + public static ResilienceStrategyTelemetry CreateTelemetry( DiagnosticSource? diagnosticSource, string? builderName, diff --git a/src/Polly.Core/Utils/CompositeResilienceStrategy.cs b/src/Polly.Core/Utils/CompositeResilienceStrategy.cs index d620791ea8e..0c5508c6c35 100644 --- a/src/Polly.Core/Utils/CompositeResilienceStrategy.cs +++ b/src/Polly.Core/Utils/CompositeResilienceStrategy.cs @@ -1,3 +1,5 @@ +using Polly.Telemetry; + namespace Polly.Utils; #pragma warning disable S2302 // "nameof" should be used @@ -10,14 +12,16 @@ namespace Polly.Utils; internal sealed partial class CompositeResilienceStrategy : ResilienceStrategy { private readonly ResilienceStrategy _firstStrategy; + private readonly ResilienceStrategyTelemetry _telemetry; + private readonly TimeProvider _timeProvider; - public static CompositeResilienceStrategy Create(IReadOnlyList strategies) + public static CompositeResilienceStrategy Create(IReadOnlyList strategies, ResilienceStrategyTelemetry telemetry, TimeProvider timeProvider) { Guard.NotNull(strategies); - if (strategies.Count < 2) + if (strategies.Count == 0) { - throw new InvalidOperationException("The composite resilience strategy must contain at least two resilience strategies."); + throw new InvalidOperationException("The composite resilience strategy must contain at least one resilience strategy."); } if (strategies.Distinct().Count() != strategies.Count) @@ -25,6 +29,11 @@ public static CompositeResilienceStrategy Create(IReadOnlyList strategies) + private CompositeResilienceStrategy(ResilienceStrategy first, IReadOnlyList strategies, ResilienceStrategyTelemetry telemetry, TimeProvider timeProvider) { Strategies = strategies; + + _telemetry = telemetry; + _timeProvider = timeProvider; _firstStrategy = first; } public IReadOnlyList Strategies { get; } - internal override ValueTask> ExecuteCore( + internal override async ValueTask> ExecuteCore( Func>> callback, - ResilienceContext context, TState state) + ResilienceContext context, + TState state) { + var timeStamp = _timeProvider.GetTimestamp(); + _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Debug, TelemetryUtil.PipelineExecuting), context, PipelineExecutingArguments.Instance); + + Outcome outcome; + if (context.CancellationToken.IsCancellationRequested) { - return Outcome.FromExceptionAsTask(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); + outcome = Outcome.FromException(new OperationCanceledException(context.CancellationToken).TrySetStackTrace()); } + else + { + outcome = await _firstStrategy.ExecuteCore(callback, context, state).ConfigureAwait(context.ContinueOnCapturedContext); + } + + var durationArgs = PipelineExecutedArguments.Get(_timeProvider.GetElapsedTime(timeStamp)); + _telemetry.Report( + new ResilienceEvent(ResilienceEventSeverity.Information, TelemetryUtil.PipelineExecuted), + new OutcomeArguments(context, outcome, durationArgs)); + PipelineExecutedArguments.Return(durationArgs); - return _firstStrategy.ExecuteCore(callback, context, state); + return outcome; } /// diff --git a/src/Polly.Extensions/README.md b/src/Polly.Extensions/README.md index 0215b86124b..839d3862a41 100644 --- a/src/Polly.Extensions/README.md +++ b/src/Polly.Extensions/README.md @@ -118,11 +118,11 @@ Dimensions: |`attempt-number`| The execution attempt number, starting at 0 (0, 1, 2). | |`attempt-handled`| Indicates if the execution outcome was handled. A handled outcome indicates execution failure and the need for retry (`true`, `false`). | -#### strategy-execution-duration +#### pipeline-execution-duration - Type: *Histogram* - Unit: *milliseconds* -- Description: Measures the duration and results of resilience strategy executions. +- Description: Measures the duration and results of resilience pipelines. Dimensions: diff --git a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs index 2469d28fe23..0e5e4ef7973 100644 --- a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs +++ b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs @@ -27,12 +27,19 @@ public ResilienceTelemetryDiagnosticSource(TelemetryOptions options) "execution-attempt-duration", unit: "ms", description: "Tracks the duration of execution attempts."); + + ExecutionDuration = Meter.CreateHistogram( + "pipeline-execution-duration", + unit: "ms", + description: "The execution duration and execution results of resilience pipelines."); } public Counter Counter { get; } public Histogram AttemptDuration { get; } + public Histogram ExecutionDuration { get; } + public override bool IsEnabled(string name) => true; #pragma warning disable IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. @@ -89,7 +96,22 @@ private void MeterEvent(TelemetryEventArguments args) { var source = args.Source; - if (args.Arguments is ExecutionAttemptArguments executionAttempt) + if (args.Arguments is PipelineExecutedArguments executionFinishedArguments) + { + if (!ExecutionDuration.Enabled) + { + return; + } + + var enrichmentContext = EnrichmentContext.Get(args.Context, null, args.Outcome); + AddCommonTags(args, source, enrichmentContext); + enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExecutionHealth, args.Context.GetExecutionHealth())); + EnrichmentUtil.Enrich(enrichmentContext, _enrichers); + + ExecutionDuration.Record(executionFinishedArguments.Duration.TotalMilliseconds, enrichmentContext.TagsSpan); + EnrichmentContext.Return(enrichmentContext); + } + else if (args.Arguments is ExecutionAttemptArguments executionAttempt) { if (!AttemptDuration.Enabled) { @@ -101,7 +123,7 @@ private void MeterEvent(TelemetryEventArguments args) enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.AttemptNumber, executionAttempt.AttemptNumber.AsBoxedInt())); enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.AttemptHandled, executionAttempt.Handled.AsBoxedBool())); EnrichmentUtil.Enrich(enrichmentContext, _enrichers); - AttemptDuration.Record(executionAttempt.ExecutionTime.TotalMilliseconds, enrichmentContext.TagsSpan); + AttemptDuration.Record(executionAttempt.Duration.TotalMilliseconds, enrichmentContext.TagsSpan); EnrichmentContext.Return(enrichmentContext); } else if (Counter.Enabled) @@ -128,7 +150,30 @@ private void LogEvent(TelemetryEventArguments args) var level = args.Event.Severity.AsLogLevel(); - if (args.Arguments is ExecutionAttemptArguments executionAttempt) + if (args.Arguments is PipelineExecutingArguments pipelineExecutionStarted) + { + _logger.ExecutingStrategy( + args.Source.BuilderName.GetValueOrPlaceholder(), + args.Source.BuilderInstanceName.GetValueOrPlaceholder(), + args.Context.OperationKey, + args.Context.GetResultType()); + } + else if (args.Arguments is PipelineExecutedArguments pipelineExecutionFinished) + { + var logLevel = args.Context.IsExecutionHealthy() ? LogLevel.Debug : LogLevel.Warning; + + _logger.StrategyExecuted( + logLevel, + args.Source.BuilderName.GetValueOrPlaceholder(), + args.Source.BuilderInstanceName.GetValueOrPlaceholder(), + args.Context.OperationKey, + args.Context.GetResultType(), + ExpandOutcome(args.Context, args.Outcome), + args.Context.GetExecutionHealth(), + pipelineExecutionFinished.Duration.TotalMilliseconds, + args.Outcome?.Exception); + } + else if (args.Arguments is ExecutionAttemptArguments executionAttempt) { if (_logger.IsEnabled(level)) { @@ -141,7 +186,7 @@ private void LogEvent(TelemetryEventArguments args) result, executionAttempt.Handled, executionAttempt.AttemptNumber, - executionAttempt.ExecutionTime.TotalMilliseconds, + executionAttempt.Duration.TotalMilliseconds, args.Outcome?.Exception); } } @@ -158,4 +203,15 @@ private void LogEvent(TelemetryEventArguments args) args.Outcome?.Exception); } } + + private object? ExpandOutcome(ResilienceContext context, Outcome? outcome) + { + if (outcome == null) + { + return null; + } + + // stryker disable once all: no means to test this + return (object)outcome.Value.Exception?.Message! ?? _resultFormatter(context, outcome.Value.Result); + } } diff --git a/src/Polly.Extensions/Telemetry/TelemetryCompositeStrategyBuilderExtensions.cs b/src/Polly.Extensions/Telemetry/TelemetryCompositeStrategyBuilderExtensions.cs index a501033f189..2888ac8e474 100644 --- a/src/Polly.Extensions/Telemetry/TelemetryCompositeStrategyBuilderExtensions.cs +++ b/src/Polly.Extensions/Telemetry/TelemetryCompositeStrategyBuilderExtensions.cs @@ -44,7 +44,6 @@ public static TBuilder ConfigureTelemetry(this TBuilder builder, ILogg /// /// Thrown when or is . [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TelemetryOptions))] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(TelemetryStrategyOptions))] public static TBuilder ConfigureTelemetry(this TBuilder builder, TelemetryOptions options) where TBuilder : CompositeStrategyBuilderBase { @@ -53,25 +52,7 @@ public static TBuilder ConfigureTelemetry(this TBuilder builder, Telem builder.Validator(new(options, $"The '{nameof(TelemetryOptions)}' are invalid.")); builder.DiagnosticSource = new ResilienceTelemetryDiagnosticSource(options); - builder.OnCreatingStrategy = strategies => - { - var telemetryStrategy = new TelemetryResilienceStrategy( - TimeProvider.System, - builder.Name, - builder.InstanceName, - options.LoggerFactory, - options.ResultFormatter, - options.Enrichers.ToList()); - -#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - strategies.Insert(0, new CompositeStrategyBuilder().AddStrategy(_ => telemetryStrategy, new TelemetryStrategyOptions()).Build()); -#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code - }; return builder; } - - private sealed class TelemetryStrategyOptions : ResilienceStrategyOptions - { - } } diff --git a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs b/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs deleted file mode 100644 index 2c090a4614e..00000000000 --- a/src/Polly.Extensions/Telemetry/TelemetryResilienceStrategy.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Diagnostics.Metrics; -using Microsoft.Extensions.Logging; - -namespace Polly.Telemetry; - -internal sealed class TelemetryResilienceStrategy : NonReactiveResilienceStrategy -{ - private readonly TimeProvider _timeProvider; - private readonly string? _builderName; - private readonly string? _builderInstance; - private readonly List> _enrichers; - private readonly ILogger _logger; - private readonly Func _resultFormatter; - - // Temporary only, until the TimeProvider is exposed - public TelemetryResilienceStrategy( - string builderName, - string? builderInstance, - ILoggerFactory loggerFactory, - Func resultFormatter, - List> enrichers) - : this(TimeProvider.System, builderName, builderInstance, loggerFactory, resultFormatter, enrichers) - { - } - - public TelemetryResilienceStrategy( - TimeProvider timeProvider, - string? builderName, - string? builderInstance, - ILoggerFactory loggerFactory, - Func resultFormatter, - List> enrichers) - { - _timeProvider = timeProvider; - _builderName = builderName; - _builderInstance = builderInstance; - _resultFormatter = resultFormatter; - _enrichers = enrichers; - _logger = loggerFactory.CreateLogger(TelemetryUtil.PollyDiagnosticSource); - ExecutionDuration = ResilienceTelemetryDiagnosticSource.Meter.CreateHistogram( - "strategy-execution-duration", - unit: "ms", - description: "The execution duration and execution results of resilience strategies."); - } - - public Histogram ExecutionDuration { get; } - - protected override async ValueTask> ExecuteCore( - Func>> callback, - ResilienceContext context, - TState state) - { - var stamp = _timeProvider.GetTimestamp(); - _logger.ExecutingStrategy(_builderName.GetValueOrPlaceholder(), _builderInstance.GetValueOrPlaceholder(), context.OperationKey, context.GetResultType()); - - var outcome = await callback(context, state).ConfigureAwait(context.ContinueOnCapturedContext); - - var duration = _timeProvider.GetElapsedTime(stamp); - var logLevel = context.IsExecutionHealthy() ? LogLevel.Debug : LogLevel.Warning; - - _logger.StrategyExecuted( - logLevel, - _builderName.GetValueOrPlaceholder(), - _builderInstance.GetValueOrPlaceholder(), - context.OperationKey, - context.GetResultType(), - ExpandOutcome(context, outcome), - context.GetExecutionHealth(), - duration.TotalMilliseconds, - outcome.Exception); - - RecordDuration(context, outcome, duration); - - return outcome; - } - - private static Outcome CreateOutcome(Outcome outcome) => outcome.HasResult ? - Outcome.FromResult(outcome.Result) : - Outcome.FromException(outcome.Exception!); - - private void RecordDuration(ResilienceContext context, Outcome outcome, TimeSpan duration) - { - if (!ExecutionDuration.Enabled) - { - return; - } - - var enrichmentContext = EnrichmentContext.Get(context, null, CreateOutcome(outcome)); - - if (_builderName is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.BuilderName, _builderName)); - } - - if (_builderInstance is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.BuilderInstance, _builderInstance)); - } - - if (context.OperationKey is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.OperationKey, context.OperationKey)); - } - - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ResultType, context.GetResultType())); - - if (outcome.Exception is Exception e) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExceptionName, e.GetType().FullName)); - } - - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExecutionHealth, context.GetExecutionHealth())); - EnrichmentUtil.Enrich(enrichmentContext, _enrichers); - - ExecutionDuration.Record(duration.TotalMilliseconds, enrichmentContext.TagsSpan); - EnrichmentContext.Return(enrichmentContext); - } - - private object? ExpandOutcome(ResilienceContext context, Outcome outcome) - { - // stryker disable once all: no means to test this - return (object)outcome.Exception?.Message! ?? _resultFormatter(context, outcome.Result); - } -} diff --git a/src/Polly.Testing/InnerStrategiesDescriptor.cs b/src/Polly.Testing/InnerStrategiesDescriptor.cs index d2b80dc6c37..7ada1a3d3de 100644 --- a/src/Polly.Testing/InnerStrategiesDescriptor.cs +++ b/src/Polly.Testing/InnerStrategiesDescriptor.cs @@ -9,12 +9,10 @@ public sealed class InnerStrategiesDescriptor /// Initializes a new instance of the class. /// /// The strategies the pipeline is composed of. - /// Determines whether the pipeline has telemetry enabled. /// Determines whether the resilience strategy is reloadable. - public InnerStrategiesDescriptor(IReadOnlyList strategies, bool hasTelemetry, bool isReloadable) + public InnerStrategiesDescriptor(IReadOnlyList strategies, bool isReloadable) { Strategies = strategies; - HasTelemetry = hasTelemetry; IsReloadable = isReloadable; } @@ -24,9 +22,9 @@ public InnerStrategiesDescriptor(IReadOnlyList str public IReadOnlyList Strategies { get; } /// - /// Gets a value indicating whether the pipeline has telemetry enabled. + /// Gets the first strategy of the pipeline. /// - public bool HasTelemetry { get; } + public ResilienceStrategyDescriptor FirstStrategy => Strategies[0]; /// /// Gets a value indicating whether the resilience strategy is reloadable. diff --git a/src/Polly.Testing/PublicAPI.Unshipped.txt b/src/Polly.Testing/PublicAPI.Unshipped.txt index 932466695a8..86b0886af86 100644 --- a/src/Polly.Testing/PublicAPI.Unshipped.txt +++ b/src/Polly.Testing/PublicAPI.Unshipped.txt @@ -1,13 +1,13 @@ #nullable enable Polly.Testing.InnerStrategiesDescriptor -Polly.Testing.InnerStrategiesDescriptor.HasTelemetry.get -> bool -Polly.Testing.InnerStrategiesDescriptor.InnerStrategiesDescriptor(System.Collections.Generic.IReadOnlyList! strategies, bool hasTelemetry, bool isReloadable) -> void +Polly.Testing.InnerStrategiesDescriptor.FirstStrategy.get -> Polly.Testing.ResilienceStrategyDescriptor! +Polly.Testing.InnerStrategiesDescriptor.InnerStrategiesDescriptor(System.Collections.Generic.IReadOnlyList! strategies, bool isReloadable) -> void Polly.Testing.InnerStrategiesDescriptor.IsReloadable.get -> bool Polly.Testing.InnerStrategiesDescriptor.Strategies.get -> System.Collections.Generic.IReadOnlyList! Polly.Testing.ResilienceStrategyDescriptor Polly.Testing.ResilienceStrategyDescriptor.Options.get -> Polly.ResilienceStrategyOptions? -Polly.Testing.ResilienceStrategyDescriptor.ResilienceStrategyDescriptor(Polly.ResilienceStrategyOptions? options, System.Type! strategyType) -> void -Polly.Testing.ResilienceStrategyDescriptor.StrategyType.get -> System.Type! +Polly.Testing.ResilienceStrategyDescriptor.ResilienceStrategyDescriptor(Polly.ResilienceStrategyOptions? options, object! strategyInstance) -> void +Polly.Testing.ResilienceStrategyDescriptor.StrategyInstance.get -> object! Polly.Testing.ResilienceStrategyExtensions static Polly.Testing.ResilienceStrategyExtensions.GetInnerStrategies(this Polly.ResilienceStrategy! strategy) -> Polly.Testing.InnerStrategiesDescriptor! static Polly.Testing.ResilienceStrategyExtensions.GetInnerStrategies(this Polly.ResilienceStrategy! strategy) -> Polly.Testing.InnerStrategiesDescriptor! diff --git a/src/Polly.Testing/ResilienceStrategyDescriptor.cs b/src/Polly.Testing/ResilienceStrategyDescriptor.cs index dbba00cfeeb..129c6f9af27 100644 --- a/src/Polly.Testing/ResilienceStrategyDescriptor.cs +++ b/src/Polly.Testing/ResilienceStrategyDescriptor.cs @@ -9,11 +9,11 @@ public sealed class ResilienceStrategyDescriptor /// Initializes a new instance of the class. /// /// The options used by the resilience strategy, if any. - /// The type of the strategy. - public ResilienceStrategyDescriptor(ResilienceStrategyOptions? options, Type strategyType) + /// The strategy instance. + public ResilienceStrategyDescriptor(ResilienceStrategyOptions? options, object strategyInstance) { Options = options; - StrategyType = strategyType; + StrategyInstance = strategyInstance; } /// @@ -22,7 +22,7 @@ public ResilienceStrategyDescriptor(ResilienceStrategyOptions? options, Type str public ResilienceStrategyOptions? Options { get; } /// - /// Gets the type of the strategy. + /// Gets the strategy instance. /// - public Type StrategyType { get; } + public object StrategyInstance { get; } } diff --git a/src/Polly.Testing/ResilienceStrategyExtensions.cs b/src/Polly.Testing/ResilienceStrategyExtensions.cs index 5a115f5c66f..4b94b70933e 100644 --- a/src/Polly.Testing/ResilienceStrategyExtensions.cs +++ b/src/Polly.Testing/ResilienceStrategyExtensions.cs @@ -7,8 +7,6 @@ namespace Polly.Testing; /// public static class ResilienceStrategyExtensions { - private const string TelemetryResilienceStrategy = "Polly.Telemetry.TelemetryResilienceStrategy"; - /// /// Gets the inner strategies the is composed of. /// @@ -41,30 +39,29 @@ private static InnerStrategiesDescriptor GetInnerStrategiesCore(ResilienceStr var strategies = new List(); strategy.ExpandStrategies(strategies); - var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, GetStrategyType(s))).ToList(); + var innerStrategies = strategies.Select(s => new ResilienceStrategyDescriptor(s.Options, GetStrategyInstance(s))).ToList(); return new InnerStrategiesDescriptor( - innerStrategies.Where(s => !ShouldSkip(s.StrategyType)).ToList().AsReadOnly(), - hasTelemetry: innerStrategies.Exists(s => s.StrategyType.FullName == TelemetryResilienceStrategy), - isReloadable: innerStrategies.Exists(s => s.StrategyType == typeof(ReloadableResilienceStrategy))); + innerStrategies.Where(s => !ShouldSkip(s.StrategyInstance)).ToList().AsReadOnly(), + isReloadable: innerStrategies.Exists(s => s.StrategyInstance is ReloadableResilienceStrategy)); } - private static Type GetStrategyType(ResilienceStrategy strategy) + private static object GetStrategyInstance(ResilienceStrategy strategy) { if (strategy is ReactiveResilienceStrategyBridge reactiveBridge) { - return reactiveBridge.Strategy.GetType(); + return reactiveBridge.Strategy; } if (strategy is NonReactiveResilienceStrategyBridge nonReactiveBridge) { - return nonReactiveBridge.Strategy.GetType(); + return nonReactiveBridge.Strategy; } - return strategy.GetType(); + return strategy; } - private static bool ShouldSkip(Type type) => type == typeof(ReloadableResilienceStrategy) || type.FullName == TelemetryResilienceStrategy; + private static bool ShouldSkip(object instance) => instance is ReloadableResilienceStrategy; private static void ExpandStrategies(this ResilienceStrategy strategy, List strategies) { diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs index 95628ed9bd4..575febe550d 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerCompositeStrategyBuilderTests.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.Time.Testing; using Polly.CircuitBreaker; -using Polly.Utils; +using Polly.Testing; namespace Polly.Core.Tests.CircuitBreaker; @@ -31,12 +31,7 @@ public void AddCircuitBreaker_Configure(Action builder builderAction(builder); - var strategy = builder.Build(); - - strategy - .Should().BeOfType>().Subject - .Strategy - .Should().BeOfType>(); + builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType>(); } [MemberData(nameof(ConfigureDataGeneric))] @@ -47,12 +42,7 @@ public void AddCircuitBreaker_Generic_Configure(Action>().Subject - .Strategy - .Should().BeOfType>(); + builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType>(); } [Fact] diff --git a/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs b/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs index 8856e8ed167..e6e50ba898c 100644 --- a/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/CompositeStrategyBuilderTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Polly.Retry; +using Polly.Testing; using Polly.Utils; namespace Polly.Core.Tests; @@ -28,7 +29,6 @@ public void CopyCtor_Ok() Name = "dummy", Randomizer = () => 0.0, DiagnosticSource = Substitute.For(), - OnCreatingStrategy = _ => { }, }; builder.Properties.Set(new ResiliencePropertyKey("dummy"), "dummy"); @@ -38,7 +38,6 @@ public void CopyCtor_Ok() other.TimeProvider.Should().Be(builder.TimeProvider); other.Randomizer.Should().BeSameAs(builder.Randomizer); other.DiagnosticSource.Should().BeSameAs(builder.DiagnosticSource); - other.OnCreatingStrategy.Should().BeSameAs(builder.OnCreatingStrategy); other.Properties.GetValue(new ResiliencePropertyKey("dummy"), "").Should().Be("dummy"); } @@ -61,7 +60,8 @@ public void AddStrategy_Single_Ok() // assert strategy.Execute(_ => executions.Add(2)); - ((NonReactiveResilienceStrategyBridge)strategy).Strategy.Should().BeOfType(); + + strategy.GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType(); executions.Should().BeInAscendingOrder(); executions.Should().HaveCount(3); } @@ -337,29 +337,6 @@ public void BuildStrategy_EnsureCorrectContext() verified2.Should().BeTrue(); } - [Fact] - public void Build_OnCreatingStrategy_EnsureRespected() - { - // arrange - var strategy = new TestResilienceStrategy().AsStrategy(); - var builder = new CompositeStrategyBuilder - { - OnCreatingStrategy = strategies => - { - strategies.Should().ContainSingle(s => s == strategy); - strategies.Insert(0, new TestResilienceStrategy().AsStrategy()); - } - }; - - builder.AddStrategy(strategy); - - // act - var finalStrategy = builder.Build(); - - // assert - finalStrategy.Should().BeOfType(); - } - [Fact] public void EmptyOptions_Ok() => CompositeStrategyBuilderExtensions.EmptyOptions.Instance.Name.Should().BeNull(); diff --git a/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs index 0b86d91e255..d428655818c 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackCompositeStrategyBuilderExtensionsTests.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Fallback; -using Polly.Utils; +using Polly.Testing; namespace Polly.Core.Tests.Fallback; @@ -25,10 +25,7 @@ public void AddFallback_Generic_Ok(Action> configu var builder = new CompositeStrategyBuilder(); configure(builder); - builder.Build().Strategy - .Should().BeOfType>().Subject - .Strategy - .Should().BeOfType>(); + builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType(typeof(FallbackResilienceStrategy)); } [Fact] diff --git a/test/Polly.Core.Tests/GenericCompositeStrategyBuilderTests.cs b/test/Polly.Core.Tests/GenericCompositeStrategyBuilderTests.cs index d3eb4504025..6cc3b69bcac 100644 --- a/test/Polly.Core.Tests/GenericCompositeStrategyBuilderTests.cs +++ b/test/Polly.Core.Tests/GenericCompositeStrategyBuilderTests.cs @@ -13,7 +13,6 @@ public void Ctor_EnsureDefaults() _builder.Name.Should().BeNull(); _builder.Properties.Should().NotBeNull(); _builder.TimeProvider.Should().Be(TimeProvider.System); - _builder.OnCreatingStrategy.Should().BeNull(); } [Fact] @@ -31,9 +30,6 @@ public void Properties_GetSet_Ok() var timeProvider = new FakeTimeProvider(); _builder.TimeProvider = timeProvider; _builder.TimeProvider.Should().Be(timeProvider); - - _builder.OnCreatingStrategy = s => { }; - _builder.OnCreatingStrategy.Should().NotBeNull(); } [Fact] @@ -63,6 +59,6 @@ public void AddGenericStrategy_Ok() // assert strategy.Should().NotBeNull(); - strategy.Strategy.Should().Be(testStrategy.Strategy); + ((CompositeResilienceStrategy)strategy.Strategy).Strategies[0].Should().Be(testStrategy.Strategy); } } diff --git a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs index c7214cffef5..7ed4b7cdf2b 100644 --- a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs +++ b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs @@ -23,7 +23,7 @@ public TaskExecutionTests() { if (args.Arguments is ExecutionAttemptArguments attempt) { - _args.Add(ExecutionAttemptArguments.Get(attempt.AttemptNumber, attempt.ExecutionTime, attempt.Handled)); + _args.Add(ExecutionAttemptArguments.Get(attempt.AttemptNumber, attempt.Duration, attempt.Handled)); } }); diff --git a/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs index a4c99ac3644..3afda1956bc 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingCompositeStrategyBuilderExtensionsTests.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Hedging; -using Polly.Utils; +using Polly.Testing; namespace Polly.Core.Tests.Hedging; @@ -14,11 +14,9 @@ public void AddHedging_Ok() { _builder.AddHedging(new HedgingStrategyOptions { ShouldHandle = _ => PredicateResult.True }); - _builder.Build() - .Should().BeOfType>().Subject - .Strategy - .Should().BeOfType>() - .Subject.HedgingHandler.IsGeneric.Should().BeFalse(); + _builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance + .Should().BeOfType>().Subject + .HedgingHandler.IsGeneric.Should().BeFalse(); } [Fact] @@ -30,11 +28,9 @@ public void AddHedging_Generic_Ok() ShouldHandle = _ => PredicateResult.True }); - _genericBuilder.Build().Strategy - .Should().BeOfType>().Subject - .Strategy - .Should().BeOfType>() - .Subject.HedgingHandler.IsGeneric.Should().BeTrue(); + _genericBuilder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance + .Should().BeOfType>().Subject + .HedgingHandler.IsGeneric.Should().BeTrue(); } [Fact] diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index 7331e4d2871..a6aabcccfb5 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -88,11 +88,11 @@ public void ExecutePrimaryAndSecondary_EnsureAttemptReported() var attempts = _events.Select(v => v.Arguments).OfType().ToArray(); attempts[0].Handled.Should().BeTrue(); - attempts[0].ExecutionTime.Should().BeGreaterThan(TimeSpan.Zero); + attempts[0].Duration.Should().BeGreaterThan(TimeSpan.Zero); attempts[0].AttemptNumber.Should().Be(0); attempts[1].Handled.Should().BeTrue(); - attempts[1].ExecutionTime.Should().BeGreaterThan(TimeSpan.Zero); + attempts[1].Duration.Should().BeGreaterThan(TimeSpan.Zero); attempts[1].AttemptNumber.Should().Be(1); } diff --git a/test/Polly.Core.Tests/Hedging/OnHedgingArgumentsTests.cs b/test/Polly.Core.Tests/Hedging/OnHedgingArgumentsTests.cs index 35e83a47086..69f0f6c813f 100644 --- a/test/Polly.Core.Tests/Hedging/OnHedgingArgumentsTests.cs +++ b/test/Polly.Core.Tests/Hedging/OnHedgingArgumentsTests.cs @@ -11,6 +11,6 @@ public void Ctor_Ok() args.AttemptNumber.Should().Be(1); args.HasOutcome.Should().BeTrue(); - args.ExecutionTime.Should().Be(TimeSpan.FromSeconds(1)); + args.Duration.Should().Be(TimeSpan.FromSeconds(1)); } } diff --git a/test/Polly.Core.Tests/Polly.Core.Tests.csproj b/test/Polly.Core.Tests/Polly.Core.Tests.csproj index 0c709602b04..51e78af5d4b 100644 --- a/test/Polly.Core.Tests/Polly.Core.Tests.csproj +++ b/test/Polly.Core.Tests/Polly.Core.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs index ae18cef3d75..da6b19f8dae 100644 --- a/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs +++ b/test/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs @@ -1,8 +1,8 @@ using System.Globalization; using Polly.Registry; using Polly.Retry; +using Polly.Testing; using Polly.Timeout; -using Polly.Utils; namespace Polly.Core.Tests.Registry; @@ -436,8 +436,8 @@ public void GetOrAddStrategy_Ok() var strategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; }); var otherStrategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; }); - ((NonReactiveResilienceStrategyBridge)strategy).Strategy.Should().BeOfType(); - strategy.Should().BeSameAs(otherStrategy); + strategy.GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType(); + called.Should().Be(1); } @@ -451,8 +451,7 @@ public void GetOrAddStrategy_Generic_Ok() var strategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; }); var otherStrategy = registry.GetOrAddStrategy(id, builder => { builder.AddTimeout(TimeSpan.FromSeconds(1)); called++; }); - ((NonReactiveResilienceStrategyBridge)strategy.Strategy).Strategy.Should().BeOfType(); - strategy.Should().BeSameAs(otherStrategy); + strategy.GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType(); } private ResilienceStrategyRegistry CreateRegistry() => new(_options); diff --git a/test/Polly.Core.Tests/ResilienceStrategyTests.cs b/test/Polly.Core.Tests/ResilienceStrategyTests.cs index b0e867942d4..9e64afdc4df 100644 --- a/test/Polly.Core.Tests/ResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/ResilienceStrategyTests.cs @@ -13,7 +13,7 @@ public void DebuggerProxy_Ok() { new TestResilienceStrategy().AsStrategy(), new TestResilienceStrategy().AsStrategy() - }); + }, null!, null!); new CompositeResilienceStrategy.DebuggerProxy(pipeline).Strategies.Should().HaveCount(2); } diff --git a/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs index fe1598cee34..54f4aa8ea75 100644 --- a/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryCompositeStrategyBuilderExtensionsTests.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using Polly.Retry; -using Polly.Utils; +using Polly.Testing; namespace Polly.Core.Tests.Retry; @@ -71,7 +71,7 @@ public void AddRetry_DefaultOptions_Ok() private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) { - var strategy = (RetryResilienceStrategy)((ReactiveResilienceStrategyBridge)builder.Build()).Strategy; + var strategy = builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); @@ -80,9 +80,14 @@ private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackof assert?.Invoke(strategy); } - private static void AssertStrategy(CompositeStrategyBuilder builder, RetryBackoffType type, int retries, TimeSpan delay, Action>? assert = null) + private static void AssertStrategy( + CompositeStrategyBuilder builder, + RetryBackoffType type, + int retries, + TimeSpan delay, + Action>? assert = null) { - var strategy = (RetryResilienceStrategy)((ReactiveResilienceStrategyBridge)builder.Build().Strategy).Strategy; + var strategy = builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType>().Subject; strategy.BackoffType.Should().Be(type); strategy.RetryCount.Should().Be(retries); diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index 6ef57f1e4b2..1bbadbf297b 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -269,7 +269,7 @@ public void Execute_EnsureAttemptReported() attempt.Handled.Should().BeFalse(); attempt.AttemptNumber.Should().Be(0); - attempt.ExecutionTime.Should().Be(TimeSpan.FromSeconds(1)); + attempt.Duration.Should().Be(TimeSpan.FromSeconds(1)); called = true; }); diff --git a/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs b/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs index 001bdeadbe3..1042b4574aa 100644 --- a/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs +++ b/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs @@ -10,7 +10,7 @@ public void Ctor_Ok() var args = new ExecutionAttemptArguments(99, TimeSpan.MaxValue, true); Assert.NotNull(args); args.AttemptNumber.Should().Be(99); - args.ExecutionTime.Should().Be(TimeSpan.MaxValue); + args.Duration.Should().Be(TimeSpan.MaxValue); args.Handled.Should().BeTrue(); } @@ -20,7 +20,7 @@ public void Get_Ok() var args = ExecutionAttemptArguments.Get(99, TimeSpan.MaxValue, true); Assert.NotNull(args); args.AttemptNumber.Should().Be(99); - args.ExecutionTime.Should().Be(TimeSpan.MaxValue); + args.Duration.Should().Be(TimeSpan.MaxValue); args.Handled.Should().BeTrue(); } @@ -32,7 +32,7 @@ public void Return_EnsurePropertiesCleared() ExecutionAttemptArguments.Return(args); args.AttemptNumber.Should().Be(0); - args.ExecutionTime.Should().Be(TimeSpan.Zero); + args.Duration.Should().Be(TimeSpan.Zero); args.Handled.Should().BeFalse(); } } diff --git a/test/Polly.Core.Tests/Telemetry/PipelineExecutedArgumentsTests.cs b/test/Polly.Core.Tests/Telemetry/PipelineExecutedArgumentsTests.cs new file mode 100644 index 00000000000..50a04240285 --- /dev/null +++ b/test/Polly.Core.Tests/Telemetry/PipelineExecutedArgumentsTests.cs @@ -0,0 +1,31 @@ +using Polly.Telemetry; + +namespace Polly.Extensions.Tests.Telemetry; + +public class PipelineExecutedArgumentsTests +{ + [Fact] + public void Ctor_Ok() + { + var args = new PipelineExecutedArguments(TimeSpan.MaxValue); + args.Duration.Should().Be(TimeSpan.MaxValue); + } + + [Fact] + public void Get_Ok() + { + var args = PipelineExecutedArguments.Get(TimeSpan.MaxValue); + Assert.NotNull(args); + args.Duration.Should().Be(TimeSpan.MaxValue); + } + + [Fact] + public void Return_EnsurePropertiesCleared() + { + var args = PipelineExecutedArguments.Get(TimeSpan.MaxValue); + + PipelineExecutedArguments.Return(args); + + args.Duration.Should().Be(TimeSpan.Zero); + } +} diff --git a/test/Polly.Core.Tests/Timeout/TimeoutCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.Core.Tests/Timeout/TimeoutCompositeStrategyBuilderExtensionsTests.cs index 1e61f5bc7ad..7a34b55376f 100644 --- a/test/Polly.Core.Tests/Timeout/TimeoutCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.Core.Tests/Timeout/TimeoutCompositeStrategyBuilderExtensionsTests.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; +using Polly.Testing; using Polly.Timeout; -using Polly.Utils; namespace Polly.Core.Tests.Timeout; @@ -33,9 +33,8 @@ internal void AddTimeout_Ok(TimeSpan timeout, Action(); configure(builder); - var strategy = ((NonReactiveResilienceStrategyBridge)builder.Build().Strategy).Strategy.Should().BeOfType().Subject; + var strategy = builder.Build().GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType().Subject; assert(strategy); - GetTimeout(strategy).Should().Be(timeout); } @@ -44,7 +43,7 @@ public void AddTimeout_Options_Ok() { var strategy = new CompositeStrategyBuilder().AddTimeout(new TimeoutStrategyOptions()).Build(); - ((NonReactiveResilienceStrategyBridge)strategy).Strategy.Should().BeOfType(); + strategy.GetInnerStrategies().FirstStrategy.StrategyInstance.Should().BeOfType(); } [Fact] diff --git a/test/Polly.Core.Tests/Utils/CompositeResilienceStrategyTests.cs b/test/Polly.Core.Tests/Utils/CompositeResilienceStrategyTests.cs index d35ebb67bef..5a6e56183f4 100644 --- a/test/Polly.Core.Tests/Utils/CompositeResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Utils/CompositeResilienceStrategyTests.cs @@ -1,20 +1,28 @@ +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Polly.Telemetry; using Polly.Utils; namespace Polly.Core.Tests.Utils; public class CompositeResilienceStrategyTests { + private readonly ResilienceStrategyTelemetry _telemetry; + private Action? _onTelemetry; + + public CompositeResilienceStrategyTests() + => _telemetry = TestUtilities.CreateResilienceTelemetry(args => _onTelemetry?.Invoke(args)); + [Fact] public void Create_ArgValidation() { - Assert.Throws(() => CompositeResilienceStrategy.Create(null!)); - Assert.Throws(() => CompositeResilienceStrategy.Create(Array.Empty())); - Assert.Throws(() => CompositeResilienceStrategy.Create(new[] { new TestResilienceStrategy().AsStrategy() })); + Assert.Throws(() => CompositeResilienceStrategy.Create(null!, null!, null!)); + Assert.Throws(() => CompositeResilienceStrategy.Create(Array.Empty(), null!, null!)); Assert.Throws(() => CompositeResilienceStrategy.Create(new ResilienceStrategy[] { NullResilienceStrategy.Instance, NullResilienceStrategy.Instance - })); + }, null!, null!)); } [Fact] @@ -27,7 +35,7 @@ public void Create_EnsureOriginalStrategiesPreserved() new TestResilienceStrategy().AsStrategy(), }; - var pipeline = CompositeResilienceStrategy.Create(strategies); + var pipeline = CreateSut(strategies); for (var i = 0; i < strategies.Length; i++) { @@ -46,7 +54,7 @@ public async Task Create_EnsureExceptionsNotWrapped() new Strategy().AsStrategy(), }; - var pipeline = CompositeResilienceStrategy.Create(strategies); + var pipeline = CreateSut(strategies); await pipeline .Invoking(p => p.ExecuteCore((_, _) => Outcome.FromResultAsTask(10), ResilienceContextPool.Shared.Get(), "state").AsTask()) .Should() @@ -63,11 +71,11 @@ public void Create_EnsurePipelineReusableAcrossDifferentPipelines() new TestResilienceStrategy().AsStrategy(), }; - var pipeline = CompositeResilienceStrategy.Create(strategies); + var pipeline = CreateSut(strategies); - CompositeResilienceStrategy.Create(new ResilienceStrategy[] { NullResilienceStrategy.Instance, pipeline }); + CreateSut(new ResilienceStrategy[] { NullResilienceStrategy.Instance, pipeline }); - this.Invoking(_ => CompositeResilienceStrategy.Create(new ResilienceStrategy[] { NullResilienceStrategy.Instance, pipeline })) + this.Invoking(_ => CreateSut(new ResilienceStrategy[] { NullResilienceStrategy.Instance, pipeline })) .Should() .NotThrow(); } @@ -83,7 +91,7 @@ public async Task Create_Cancelled_EnsureNoExecution() new TestResilienceStrategy().AsStrategy(), }; - var pipeline = CompositeResilienceStrategy.Create(strategies); + var pipeline = CreateSut(strategies, new FakeTimeProvider()); var context = ResilienceContextPool.Shared.Get(); context.CancellationToken = cancellation.Token; @@ -101,8 +109,7 @@ public async Task Create_CancelledLater_EnsureNoExecution() new TestResilienceStrategy { Before = (_, _) => { executed = true; cancellation.Cancel(); } }.AsStrategy(), new TestResilienceStrategy().AsStrategy(), }; - - var pipeline = CompositeResilienceStrategy.Create(strategies); + var pipeline = CreateSut(strategies, new FakeTimeProvider()); var context = ResilienceContextPool.Shared.Get(); context.CancellationToken = cancellation.Token; @@ -111,6 +118,35 @@ public async Task Create_CancelledLater_EnsureNoExecution() executed.Should().BeTrue(); } + [Fact] + public void ExecuptePipeline_EnsureTelemetryArgumentsReported() + { + var items = new List(); + var timeProvider = new FakeTimeProvider(); + + _onTelemetry = args => + { + if (args.Arguments is PipelineExecutedArguments executed) + { + executed.Duration.Should().Be(TimeSpan.FromHours(1)); + } + + items.Add(args.Arguments); + }; + + var pipeline = CreateSut(new[] { new TestResilienceStrategy().AsStrategy() }, timeProvider); + pipeline.Execute(() => { timeProvider.Advance(TimeSpan.FromHours(1)); }); + + items.Should().HaveCount(2); + items[0].Should().Be(PipelineExecutingArguments.Instance); + items[1].Should().BeOfType(); + } + + private CompositeResilienceStrategy CreateSut(ResilienceStrategy[] strategies, TimeProvider? timeProvider = null) + { + return CompositeResilienceStrategy.Create(strategies, _telemetry, timeProvider ?? Substitute.For()); + } + private class Strategy : NonReactiveResilienceStrategy { protected internal override async ValueTask> ExecuteCore( diff --git a/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs b/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs index 39fbe6a4589..c40961b0c6d 100644 --- a/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs +++ b/test/Polly.Extensions.Tests/Issues/IssuesTests.StrategiesPerEndpoint_1365.cs @@ -98,7 +98,7 @@ public void StrategiesPerEndpoint_1365() provider.GetStrategy(resource2Key).Should().BeSameAs(strategy2); strategy1.Execute(() => { }); - events.Should().HaveCount(3); + events.Should().HaveCount(5); events[0].Tags["builder-name"].Should().Be("endpoint-pipeline"); events[0].Tags["builder-instance"].Should().Be("Endpoint 1/Resource 1"); } diff --git a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs index 1268e5c3925..8b96cb873c2 100644 --- a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs @@ -37,6 +37,9 @@ public void Meter_Ok() source.Counter.Description.Should().Be("Tracks the number of resilience events that occurred in resilience strategies."); source.AttemptDuration.Description.Should().Be("Tracks the duration of execution attempts."); source.AttemptDuration.Unit.Should().Be("ms"); + + source.ExecutionDuration.Description.Should().Be("The execution duration and execution results of resilience pipelines."); + source.ExecutionDuration.Unit.Should().Be("ms"); } [Fact] @@ -365,6 +368,137 @@ public void OnTelemetryEvent_Ok(bool hasCallback) called.Should().Be(hasCallback); } + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + [Theory] + public void PipelineExecution_Logged(bool healthy, bool exception) + { + var healthString = healthy ? "Healthy" : "Unhealthy"; + var context = ResilienceContextPool.Shared.Get("op-key").WithResultType(); + var telemetry = Create(); + var outcome = exception ? Outcome.FromException(new InvalidOperationException("dummy message")) : Outcome.FromResult((object)10); + var result = exception ? "dummy message" : "10"; + + if (!healthy) + { + ((List)context.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); + } + + ReportEvent(telemetry, outcome: outcome, arg: new PipelineExecutingArguments(), context: context); + ReportEvent(telemetry, outcome: outcome, arg: new PipelineExecutedArguments(TimeSpan.FromSeconds(10)), context: context); + + var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); + messages.Should().HaveCount(1); + messages[0].Message.Should().Be("Resilience strategy executing. Source: 'my-builder/builder-instance', Operation Key: 'op-key', Result Type: 'Int32'"); + messages = _logger.GetRecords(new EventId(2, "StrategyExecuted")).ToList(); + messages.Should().HaveCount(1); + messages[0].Message.Should().Match($"Resilience strategy executed. Source: 'my-builder/builder-instance', Operation Key: 'op-key', Result Type: 'Int32', Result: '{result}', Execution Health: '{healthString}', Execution Time: 10000ms"); + messages[0].LogLevel.Should().Be(healthy ? LogLevel.Debug : LogLevel.Warning); + } + + [Fact] + public void PipelineExecution_VoidResult_Ok() + { + var context = ResilienceContextPool.Shared.Get("op-key").WithVoidResultType(); + var telemetry = Create(); + ReportEvent(telemetry, outcome: null, arg: new PipelineExecutingArguments(), context: context); + + var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); + messages.Should().HaveCount(1); + messages[0].Message.Should().Be("Resilience strategy executing. Source: 'my-builder/builder-instance', Operation Key: 'op-key', Result Type: 'void'"); + } + + [Fact] + public void PipelineExecution_NoOutcome_Logged() + { + var context = ResilienceContextPool.Shared.Get("op-key").WithResultType(); + var telemetry = Create(); + + ReportEvent(telemetry, outcome: null, arg: new PipelineExecutedArguments(TimeSpan.FromSeconds(10)), context: context); + + var messages = _logger.GetRecords(new EventId(2, "StrategyExecuted")).ToList(); + messages[0].Message.Should().Match($"Resilience strategy executed. Source: 'my-builder/builder-instance', Operation Key: 'op-key', Result Type: 'Int32', Result: '', Execution Health: 'Healthy', Execution Time: 10000ms"); + } + + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + [Theory] + public void PipelineExecution_Metered(bool healthy, bool exception) + { + var healthString = healthy ? "Healthy" : "Unhealthy"; + var context = ResilienceContextPool.Shared.Get("op-key").WithResultType(); + var outcome = exception ? Outcome.FromException(new InvalidOperationException("dummy message")) : Outcome.FromResult((object)10); + var result = exception ? "dummy message" : "10"; + + if (!healthy) + { + ((List)context.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); + } + + var telemetry = Create(enrichers => + { + enrichers.Add(context => + { + if (exception) + { + context.Outcome!.Value.Exception.Should().BeOfType(); + } + + context.Tags.Add(new("custom-tag", "custom-tag-value")); + }); + }); + + ReportEvent(telemetry, outcome: outcome, arg: new PipelineExecutedArguments(TimeSpan.FromSeconds(10)), context: context); + + var ev = _events.Single(v => v.Name == "pipeline-execution-duration").Tags; + + ev.Count.Should().Be(exception ? 10 : 9); + ev["builder-instance"].Should().Be("builder-instance"); + ev["operation-key"].Should().Be("op-key"); + ev["builder-name"].Should().Be("my-builder"); + ev["result-type"].Should().Be("Int32"); + ev["event-name"].Should().Be("my-event"); + ev["event-severity"].Should().Be("Warning"); + ev["strategy-name"].Should().Be("my-strategy"); + ev["custom-tag"].Should().Be("custom-tag-value"); + + if (exception) + { + ev["exception-name"].Should().Be("System.InvalidOperationException"); + } + else + { + ev.Should().NotContainKey("exception-name"); + } + + if (healthy) + { + ev["execution-health"].Should().Be("Healthy"); + } + else + { + ev["execution-health"].Should().Be("Unhealthy"); + } + } + + [Fact] + public void PipelineExecuted_ShouldBeSkipped() + { + _metering.Dispose(); + _metering = TestUtilities.EnablePollyMetering(_events, _ => false); + + var telemetry = Create(); + var attemptArg = new PipelineExecutedArguments(TimeSpan.FromSeconds(50)); + ReportEvent(telemetry, Outcome.FromResult(true), context: ResilienceContextPool.Shared.Get("op-key").WithResultType(), arg: attemptArg); + + var events = GetEvents("pipeline-execution-duration"); + events.Should().HaveCount(0); + } + private List> GetEvents(string eventName) => _events.Where(e => e.Name == eventName).Select(v => v.Tags).ToList(); private ResilienceTelemetryDiagnosticSource Create(Action>>? configureEnrichers = null) diff --git a/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyTests.cs b/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyTests.cs deleted file mode 100644 index 1fc6d37976e..00000000000 --- a/test/Polly.Extensions.Tests/Telemetry/TelemetryResilienceStrategyTests.cs +++ /dev/null @@ -1,232 +0,0 @@ -using Microsoft.Extensions.Logging; -using Polly.Telemetry; - -namespace Polly.Extensions.Tests.Telemetry; - -#pragma warning disable S103 // Lines should not be too long - -[Collection(nameof(NonParallelizableCollection))] -public class TelemetryResilienceStrategyTests : IDisposable -{ - private readonly FakeLogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IDisposable _metering; - private readonly List _events = new(); - private Action _enricher = _ => { }; - - public TelemetryResilienceStrategyTests() - { - _loggerFactory = TestUtilities.CreateLoggerFactory(out _logger); - _metering = TestUtilities.EnablePollyMetering(_events); - } - - [Fact] - public void Ctor_Ok() - { - var strategy = CreateStrategy(); - var duration = ((TelemetryResilienceStrategy)strategy.GetType().GetRuntimeProperty("Strategy")!.GetValue(strategy)!).ExecutionDuration; - - duration.Unit.Should().Be("ms"); - duration.Description.Should().Be("The execution duration and execution results of resilience strategies."); - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public void Execute_EnsureLogged(bool healthy) - { - var healthString = healthy ? "Healthy" : "Unhealthy"; - var strategy = CreateStrategy(); - - strategy.Execute( - (c, _) => - { - if (!healthy) - { - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); - } - }, - ResilienceContextPool.Shared.Get("op-key"), string.Empty); - - var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); - messages.Should().HaveCount(1); - messages[0].Message.Should().Be("Resilience strategy executing. Source: 'my-builder/my-instance', Operation Key: 'op-key', Result Type: 'void'"); - messages = _logger.GetRecords(new EventId(2, "StrategyExecuted")).ToList(); - messages.Should().HaveCount(1); - messages[0].Message.Should().Match($"Resilience strategy executed. Source: 'my-builder/my-instance', Operation Key: 'op-key', Result Type: 'void', Result: 'void', Execution Health: '{healthString}', Execution Time: *ms"); - messages[0].LogLevel.Should().Be(healthy ? LogLevel.Debug : LogLevel.Warning); - - // verify reported state - var coll = messages[0].State.Should().BeAssignableTo>>().Subject; - coll.Count.Should().Be(8); - coll.AsEnumerable().Should().HaveCount(8); - (coll as IEnumerable).GetEnumerator().Should().NotBeNull(); - - for (int i = 0; i < coll.Count; i++) - { - coll[i].Value.Should().NotBeNull(); - } - - coll.Invoking(c => c[coll.Count + 1]).Should().Throw(); - } - - [Fact] - public void Execute_WithException_EnsureLogged() - { - var strategy = CreateStrategy(); - strategy.Invoking(s => s.Execute(_ => throw new InvalidOperationException("Dummy message."), ResilienceContextPool.Shared.Get("op-key"))).Should().Throw(); - - var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); - messages.Should().HaveCount(1); - messages[0].Message.Should().Be("Resilience strategy executing. Source: 'my-builder/my-instance', Operation Key: 'op-key', Result Type: 'void'"); - - messages = _logger.GetRecords(new EventId(2, "StrategyExecuted")).ToList(); - messages.Should().HaveCount(1); - messages[0].Message.Should().Match($"Resilience strategy executed. Source: 'my-builder/my-instance', Operation Key: 'op-key', Result Type: 'void', Result: 'Dummy message.', Execution Health: 'Healthy', Execution Time: *ms"); - messages[0].Exception.Should().BeOfType(); - } - - [Fact] - public void Execute_WithException_EnsureEnrichmentContextWithCorrectOutcome() - { - var strategy = CreateStrategy(); - - _enricher = c => - { - c.Outcome!.Value.Exception.Should().BeOfType(); - }; - strategy.Invoking(s => s.Execute(_ => throw new InvalidOperationException("Dummy message."))).Should().Throw(); - } - - [Fact] - public void Execute_WithResult_EnsureEnrichmentContextWithCorrectOutcome() - { - var strategy = CreateStrategy(); - - _enricher = c => - { - c.Outcome!.Value.Result.Should().Be("dummy"); - }; - - strategy.Execute(_ => "dummy"); - } - - [Fact] - public void Execute_WithException_EnsureMetered() - { - var strategy = CreateStrategy(); - strategy.Invoking(s => s.Execute(_ => throw new InvalidOperationException("Dummy message."), ResilienceContextPool.Shared.Get("op-key"))).Should().Throw(); - - var ev = _events.Single(v => v.Name == "strategy-execution-duration").Tags; - - ev.Count.Should().Be(6); - ev["builder-instance"].Should().Be("my-instance"); - ev["operation-key"].Should().Be("op-key"); - ev["builder-name"].Should().Be("my-builder"); - ev["result-type"].Should().Be("void"); - ev["exception-name"].Should().Be("System.InvalidOperationException"); - ev["execution-health"].Should().Be("Healthy"); - } - - [Fact] - public void Execute_Enrichers_Ok() - { - _enricher = context => - { - context.Tags.Add(new KeyValuePair("my-custom-tag", "my-tag-value")); - }; - var strategy = CreateStrategy(); - strategy.Execute(_ => true); - - var ev = _events.Single(v => v.Name == "strategy-execution-duration").Tags; - - ev.Count.Should().Be(5); - ev["my-custom-tag"].Should().Be("my-tag-value"); - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public void Execute_WithResult_EnsureMetered(bool healthy) - { - var strategy = CreateStrategy(); - strategy.Execute( - (c, _) => - { - if (!healthy) - { - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); - } - - return true; - }, - ResilienceContextPool.Shared.Get("op-key"), string.Empty); - - var ev = _events.Single(v => v.Name == "strategy-execution-duration").Tags; - - ev.Count.Should().Be(5); - ev["builder-instance"].Should().Be("my-instance"); - ev["operation-key"].Should().Be("op-key"); - ev["builder-name"].Should().Be("my-builder"); - ev["result-type"].Should().Be("Boolean"); - ev.Should().NotContainKey("exception-name"); - - if (healthy) - { - ev["execution-health"].Should().Be("Healthy"); - } - else - { - ev["execution-health"].Should().Be("Unhealthy"); - } - } - - [InlineData(true)] - [InlineData(false)] - [Theory] - public void Execute_ExecutionHealth(bool healthy) - { - var strategy = CreateStrategy(); - strategy.Execute( - (c, _) => - { - if (healthy) - { - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Information, "dummy")); - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Information, "dummy")); - } - else - { - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Information, "dummy")); - ((List)c.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); - } - - return true; - }, - ResilienceContextPool.Shared.Get(), string.Empty); - - var ev = _events.Single(v => v.Name == "strategy-execution-duration").Tags; - - if (healthy) - { - ev["execution-health"].Should().Be("Healthy"); - } - else - { - ev["execution-health"].Should().Be("Unhealthy"); - } - } - - private ResilienceStrategy CreateStrategy() - { - return new CompositeStrategyBuilder() - .AddStrategy(_ => new TelemetryResilienceStrategy("my-builder", "my-instance", _loggerFactory, (_, r) => r, new List> { c => _enricher?.Invoke(c) }), new TestResilienceStrategyOptions()) - .Build(); - } - - public void Dispose() - { - _metering.Dispose(); - _loggerFactory.Dispose(); - } -} diff --git a/test/Polly.Extensions.Tests/Utils/ResilienceContextExtensionsTests.cs b/test/Polly.Extensions.Tests/Utils/ResilienceContextExtensionsTests.cs new file mode 100644 index 00000000000..e6209416c64 --- /dev/null +++ b/test/Polly.Extensions.Tests/Utils/ResilienceContextExtensionsTests.cs @@ -0,0 +1,31 @@ +using Polly.Telemetry; + +namespace Polly.Extensions.Tests.Utils; + +public class ResilienceContextExtensionsTests +{ + [Fact] + public void IsHealthy_Ok() + { + var context = ResilienceContextPool.Shared.Get(); + AddEvent(context, ResilienceEventSeverity.Warning); + context.IsExecutionHealthy().Should().BeFalse(); + + context = ResilienceContextPool.Shared.Get(); + context.IsExecutionHealthy().Should().BeTrue(); + + context = ResilienceContextPool.Shared.Get(); + AddEvent(context, ResilienceEventSeverity.Information); + context.IsExecutionHealthy().Should().BeTrue(); + + context = ResilienceContextPool.Shared.Get(); + AddEvent(context, ResilienceEventSeverity.Information); + AddEvent(context, ResilienceEventSeverity.Warning); + context.IsExecutionHealthy().Should().BeFalse(); + } + + private static void AddEvent(ResilienceContext context, ResilienceEventSeverity severity) + { + ((List)context.ResilienceEvents).Add(new ResilienceEvent(severity, "dummy")); + } +} diff --git a/test/Polly.RateLimiting.Tests/RateLimiterCompositeStrategyBuilderExtensionsTests.cs b/test/Polly.RateLimiting.Tests/RateLimiterCompositeStrategyBuilderExtensionsTests.cs index ae9582e54f0..7abb60271cf 100644 --- a/test/Polly.RateLimiting.Tests/RateLimiterCompositeStrategyBuilderExtensionsTests.cs +++ b/test/Polly.RateLimiting.Tests/RateLimiterCompositeStrategyBuilderExtensionsTests.cs @@ -86,10 +86,11 @@ public void AddRateLimiter_Ok() RateLimiter = ResilienceRateLimiter.Create(limiter) }) .Build() - .GetInnerStrategies().Strategies.Single() - .StrategyType + .GetInnerStrategies() + .FirstStrategy + .StrategyInstance .Should() - .Be(); + .BeOfType(); } [Fact] @@ -129,17 +130,19 @@ public void AddRateLimiter_Options_Ok() RateLimiter = ResilienceRateLimiter.Create(Substitute.For()) }) .Build() - .GetInnerStrategies().Strategies - .Single() - .StrategyType + .GetInnerStrategies() + .FirstStrategy + .StrategyInstance .Should() - .Be(); + .BeOfType(); } private static void AssertRateLimiterStrategy(CompositeStrategyBuilder builder, Action? assert = null, bool hasEvents = false) { ResilienceStrategy strategy = builder.Build(); - var limiterStrategy = GetResilienceStrategy(strategy); + + var limiterStrategy = (RateLimiterResilienceStrategy)strategy.GetInnerStrategies().FirstStrategy.StrategyInstance; + assert?.Invoke(limiterStrategy); if (hasEvents) @@ -154,11 +157,11 @@ private static void AssertRateLimiterStrategy(CompositeStrategyBuilder builder, limiterStrategy.OnLeaseRejected.Should().BeNull(); } - strategy.GetInnerStrategies().Strategies.Single().StrategyType.Should().Be(typeof(RateLimiterResilienceStrategy)); - } - - private static RateLimiterResilienceStrategy GetResilienceStrategy(ResilienceStrategy strategy) - { - return (RateLimiterResilienceStrategy)strategy.GetType().GetRuntimeProperty("Strategy")!.GetValue(strategy)!; + strategy + .GetInnerStrategies() + .FirstStrategy + .StrategyInstance + .Should() + .BeOfType(); } } diff --git a/test/Polly.TestUtils/TestUtilities.cs b/test/Polly.TestUtils/TestUtilities.cs index 1ea4cdd3ca1..48db532993b 100644 --- a/test/Polly.TestUtils/TestUtilities.cs +++ b/test/Polly.TestUtils/TestUtilities.cs @@ -112,6 +112,12 @@ public static ResilienceContext WithResultType(this ResilienceContext context return context; } + public static ResilienceContext WithVoidResultType(this ResilienceContext context) + { + context.Initialize(true); + return context; + } + private sealed class CallbackDiagnosticSource : DiagnosticSource { private readonly Action _callback; @@ -128,7 +134,7 @@ public override void Write(string name, object? value) if (arguments is ExecutionAttemptArguments attempt) { - arguments = ExecutionAttemptArguments.Get(attempt.AttemptNumber, attempt.ExecutionTime, attempt.Handled); + arguments = ExecutionAttemptArguments.Get(attempt.AttemptNumber, attempt.Duration, attempt.Handled); } // copy the args because these are pooled and in tests we want to preserve them diff --git a/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs index baf4f8c12cd..097290e7eb0 100644 --- a/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs +++ b/test/Polly.Testing.Tests/ResilienceStrategyExtensionsTests.cs @@ -33,27 +33,27 @@ public void GetInnerStrategies_Generic_Ok() var descriptor = strategy.GetInnerStrategies(); // assert - descriptor.HasTelemetry.Should().BeTrue(); descriptor.IsReloadable.Should().BeFalse(); descriptor.Strategies.Should().HaveCount(7); + descriptor.FirstStrategy.Options.Should().BeOfType>(); descriptor.Strategies[0].Options.Should().BeOfType>(); - descriptor.Strategies[0].StrategyType.FullName.Should().Contain("Fallback"); + descriptor.Strategies[0].StrategyInstance.GetType().FullName.Should().Contain("Fallback"); descriptor.Strategies[1].Options.Should().BeOfType>(); - descriptor.Strategies[1].StrategyType.FullName.Should().Contain("Retry"); + descriptor.Strategies[1].StrategyInstance.GetType().FullName.Should().Contain("Retry"); descriptor.Strategies[2].Options.Should().BeOfType>(); - descriptor.Strategies[2].StrategyType.FullName.Should().Contain("CircuitBreaker"); + descriptor.Strategies[2].StrategyInstance.GetType().FullName.Should().Contain("CircuitBreaker"); descriptor.Strategies[3].Options.Should().BeOfType(); - descriptor.Strategies[3].StrategyType.FullName.Should().Contain("Timeout"); + descriptor.Strategies[3].StrategyInstance.GetType().FullName.Should().Contain("Timeout"); descriptor.Strategies[3].Options .Should() .BeOfType().Subject.Timeout .Should().Be(TimeSpan.FromSeconds(1)); descriptor.Strategies[4].Options.Should().BeOfType>(); - descriptor.Strategies[4].StrategyType.FullName.Should().Contain("Hedging"); + descriptor.Strategies[4].StrategyInstance.GetType().FullName.Should().Contain("Hedging"); descriptor.Strategies[5].Options.Should().BeOfType(); - descriptor.Strategies[5].StrategyType.FullName.Should().Contain("RateLimiter"); - descriptor.Strategies[6].StrategyType.Should().Be(typeof(CustomStrategy)); + descriptor.Strategies[5].StrategyInstance.GetType().FullName.Should().Contain("RateLimiter"); + descriptor.Strategies[6].StrategyInstance.GetType().Should().Be(typeof(CustomStrategy)); } [Fact] @@ -73,23 +73,22 @@ public void GetInnerStrategies_NonGeneric_Ok() var descriptor = strategy.GetInnerStrategies(); // assert - descriptor.HasTelemetry.Should().BeTrue(); descriptor.IsReloadable.Should().BeFalse(); descriptor.Strategies.Should().HaveCount(5); descriptor.Strategies[0].Options.Should().BeOfType(); - descriptor.Strategies[0].StrategyType.FullName.Should().Contain("Retry"); + descriptor.Strategies[0].StrategyInstance.GetType().FullName.Should().Contain("Retry"); descriptor.Strategies[1].Options.Should().BeOfType(); - descriptor.Strategies[1].StrategyType.FullName.Should().Contain("CircuitBreaker"); + descriptor.Strategies[1].StrategyInstance.GetType().FullName.Should().Contain("CircuitBreaker"); descriptor.Strategies[2].Options.Should().BeOfType(); - descriptor.Strategies[2].StrategyType.FullName.Should().Contain("Timeout"); + descriptor.Strategies[2].StrategyInstance.GetType().FullName.Should().Contain("Timeout"); descriptor.Strategies[2].Options .Should() .BeOfType().Subject.Timeout .Should().Be(TimeSpan.FromSeconds(1)); descriptor.Strategies[3].Options.Should().BeOfType(); - descriptor.Strategies[3].StrategyType.FullName.Should().Contain("RateLimiter"); - descriptor.Strategies[4].StrategyType.Should().Be(typeof(CustomStrategy)); + descriptor.Strategies[3].StrategyInstance.GetType().FullName.Should().Contain("RateLimiter"); + descriptor.Strategies[4].StrategyInstance.GetType().Should().Be(typeof(CustomStrategy)); } [Fact] @@ -104,7 +103,6 @@ public void GetInnerStrategies_SingleStrategy_Ok() var descriptor = strategy.GetInnerStrategies(); // assert - descriptor.HasTelemetry.Should().BeFalse(); descriptor.IsReloadable.Should().BeFalse(); descriptor.Strategies.Should().HaveCount(1); descriptor.Strategies[0].Options.Should().BeOfType(); @@ -127,11 +125,10 @@ public void GetInnerStrategies_Reloadable_Ok() var descriptor = strategy.GetInnerStrategies(); // assert - descriptor.HasTelemetry.Should().BeFalse(); descriptor.IsReloadable.Should().BeTrue(); descriptor.Strategies.Should().HaveCount(2); descriptor.Strategies[0].Options.Should().BeOfType(); - descriptor.Strategies[1].StrategyType.Should().Be(typeof(CustomStrategy)); + descriptor.Strategies[1].StrategyInstance.GetType().Should().Be(typeof(CustomStrategy)); } private sealed class CustomStrategy : NonReactiveResilienceStrategy