diff --git a/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs b/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs index daf8b0244a8..e4ca157469f 100644 --- a/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs +++ b/bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs @@ -47,17 +47,7 @@ private ResiliencePipeline Build(ResiliencePipelineBuilder builder) if (Enrichment) { - options.Enrichers.Add(context => - { - // The Microsoft.Extensions.Resilience library will add around 6 additional tags - // https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.Resilience - context.Tags.Add(new("dummy1", "dummy")); - context.Tags.Add(new("dummy2", "dummy")); - context.Tags.Add(new("dummy3", "dummy")); - context.Tags.Add(new("dummy4", "dummy")); - context.Tags.Add(new("dummy5", "dummy")); - context.Tags.Add(new("dummy6", "dummy")); - }); + options.MeteringEnrichers.Add(new CustomEnricher()); } builder.ConfigureTelemetry(options); @@ -66,6 +56,21 @@ private ResiliencePipeline Build(ResiliencePipelineBuilder builder) return builder.Build(); } + private class CustomEnricher : MeteringEnricher + { + public override void Enrich(in EnrichmentContext context) + { + // The Microsoft.Extensions.Resilience library will add around 6 additional tags + // https://github.com/dotnet/extensions/tree/main/src/Libraries/Microsoft.Extensions.Resilience + context.Tags.Add(new("dummy1", "dummy")); + context.Tags.Add(new("dummy2", "dummy")); + context.Tags.Add(new("dummy3", "dummy")); + context.Tags.Add(new("dummy4", "dummy")); + context.Tags.Add(new("dummy5", "dummy")); + context.Tags.Add(new("dummy6", "dummy")); + } + } + private class TelemetryEventStrategy : ResilienceStrategy { private readonly ResilienceStrategyTelemetry _telemetry; diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs index 42d33d9c1ac..fee5bb828f6 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitClosedArguments.cs @@ -1,12 +1,14 @@ namespace Polly.CircuitBreaker; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments used by event. /// -public sealed class OnCircuitClosedArguments +public readonly struct OnCircuitClosedArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// Indicates whether the circuit was closed manually by using . public OnCircuitClosedArguments(bool isManual) => IsManual = isManual; diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs index 1dbc56a1e5d..b2957daa213 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitHalfOpenedArguments.cs @@ -1,12 +1,14 @@ namespace Polly.CircuitBreaker; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments used by event. /// -public sealed class OnCircuitHalfOpenedArguments +public readonly struct OnCircuitHalfOpenedArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The context instance. public OnCircuitHalfOpenedArguments(ResilienceContext context) => Context = context; diff --git a/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs index 3f4b7f89efe..ba0d92af0e6 100644 --- a/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs +++ b/src/Polly.Core/CircuitBreaker/OnCircuitOpenedArguments.cs @@ -1,12 +1,14 @@ namespace Polly.CircuitBreaker; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments used by event. /// -public sealed class OnCircuitOpenedArguments +public readonly struct OnCircuitOpenedArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The duration of break. /// Indicates whether the circuit was opened manually by using . diff --git a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs index c9d57081db3..518a84bec11 100644 --- a/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs +++ b/src/Polly.Core/Fallback/FallbackResilienceStrategy.cs @@ -26,7 +26,7 @@ protected internal override async ValueTask> ExecuteCore(Func return outcome; } - var onFallbackArgs = new OutcomeArguments(context, outcome, new OnFallbackArguments()); + var onFallbackArgs = new OutcomeArguments(context, outcome, default); _telemetry.Report(new(ResilienceEventSeverity.Warning, FallbackConstants.OnFallback), onFallbackArgs); diff --git a/src/Polly.Core/Fallback/OnFallbackArguments.cs b/src/Polly.Core/Fallback/OnFallbackArguments.cs index c3452616a0c..4105d0fa0a3 100644 --- a/src/Polly.Core/Fallback/OnFallbackArguments.cs +++ b/src/Polly.Core/Fallback/OnFallbackArguments.cs @@ -3,6 +3,6 @@ namespace Polly.Fallback; /// /// Represents arguments used in fallback handling scenarios. /// -public sealed class OnFallbackArguments +public readonly struct OnFallbackArguments { } diff --git a/src/Polly.Core/Hedging/OnHedgingArguments.cs b/src/Polly.Core/Hedging/OnHedgingArguments.cs index 04ab9d94045..190549506da 100644 --- a/src/Polly.Core/Hedging/OnHedgingArguments.cs +++ b/src/Polly.Core/Hedging/OnHedgingArguments.cs @@ -1,12 +1,14 @@ namespace Polly.Hedging; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Represents arguments used by the on-hedging event. /// -public sealed class OnHedgingArguments +public readonly struct OnHedgingArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The zero-based hedging attempt number. /// Indicates whether outcome is available. diff --git a/src/Polly.Core/PublicAPI.Unshipped.txt b/src/Polly.Core/PublicAPI.Unshipped.txt index 8516af72bb3..e99c5acfe65 100644 --- a/src/Polly.Core/PublicAPI.Unshipped.txt +++ b/src/Polly.Core/PublicAPI.Unshipped.txt @@ -5,6 +5,7 @@ abstract Polly.ResilienceContextPool.Get(string? operationKey, System.Threading. abstract Polly.ResilienceContextPool.Return(Polly.ResilienceContext! context) -> void abstract Polly.ResilienceStrategy.ExecuteCore(System.Func>>! callback, Polly.ResilienceContext! context, TState state) -> System.Threading.Tasks.ValueTask> abstract Polly.ResilienceStrategy.ExecuteCore(System.Func>>! callback, Polly.ResilienceContext! context, TState state) -> System.Threading.Tasks.ValueTask> +abstract Polly.Telemetry.TelemetryListener.Write(in Polly.Telemetry.TelemetryEventArguments args) -> void override Polly.Outcome.ToString() -> string! override Polly.Registry.ResiliencePipelineRegistry.TryGetPipeline(TKey key, out Polly.ResiliencePipeline? pipeline) -> bool override Polly.Registry.ResiliencePipelineRegistry.TryGetPipeline(TKey key, out Polly.ResiliencePipeline? pipeline) -> bool @@ -44,11 +45,11 @@ Polly.CircuitBreaker.CircuitBreakerStrategyOptions.ManualControl.get -> Polly.CircuitBreaker.CircuitBreakerStrategyOptions.ManualControl.set -> void Polly.CircuitBreaker.CircuitBreakerStrategyOptions.MinimumThroughput.get -> int Polly.CircuitBreaker.CircuitBreakerStrategyOptions.MinimumThroughput.set -> void -Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnClosed.get -> System.Func, System.Threading.Tasks.ValueTask>? +Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnClosed.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnClosed.set -> void -Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnHalfOpened.get -> System.Func? +Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnHalfOpened.get -> System.Func? Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnHalfOpened.set -> void -Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnOpened.get -> System.Func, System.Threading.Tasks.ValueTask>? +Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnOpened.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.CircuitBreaker.CircuitBreakerStrategyOptions.OnOpened.set -> void Polly.CircuitBreaker.CircuitBreakerStrategyOptions.SamplingDuration.get -> System.TimeSpan Polly.CircuitBreaker.CircuitBreakerStrategyOptions.SamplingDuration.set -> void @@ -67,13 +68,16 @@ Polly.CircuitBreaker.IsolatedCircuitException.IsolatedCircuitException(string! m Polly.CircuitBreaker.IsolatedCircuitException.IsolatedCircuitException(string! message, System.Exception! innerException) -> void Polly.CircuitBreaker.OnCircuitClosedArguments Polly.CircuitBreaker.OnCircuitClosedArguments.IsManual.get -> bool +Polly.CircuitBreaker.OnCircuitClosedArguments.OnCircuitClosedArguments() -> void Polly.CircuitBreaker.OnCircuitClosedArguments.OnCircuitClosedArguments(bool isManual) -> void Polly.CircuitBreaker.OnCircuitHalfOpenedArguments Polly.CircuitBreaker.OnCircuitHalfOpenedArguments.Context.get -> Polly.ResilienceContext! +Polly.CircuitBreaker.OnCircuitHalfOpenedArguments.OnCircuitHalfOpenedArguments() -> void Polly.CircuitBreaker.OnCircuitHalfOpenedArguments.OnCircuitHalfOpenedArguments(Polly.ResilienceContext! context) -> void Polly.CircuitBreaker.OnCircuitOpenedArguments Polly.CircuitBreaker.OnCircuitOpenedArguments.BreakDuration.get -> System.TimeSpan Polly.CircuitBreaker.OnCircuitOpenedArguments.IsManual.get -> bool +Polly.CircuitBreaker.OnCircuitOpenedArguments.OnCircuitOpenedArguments() -> void Polly.CircuitBreaker.OnCircuitOpenedArguments.OnCircuitOpenedArguments(System.TimeSpan breakDuration, bool isManual) -> void Polly.CircuitBreakerResiliencePipelineBuilderExtensions Polly.ExecutionRejectedException @@ -86,7 +90,7 @@ Polly.Fallback.FallbackStrategyOptions Polly.Fallback.FallbackStrategyOptions.FallbackAction.get -> System.Func, System.Threading.Tasks.ValueTask>>? Polly.Fallback.FallbackStrategyOptions.FallbackAction.set -> void Polly.Fallback.FallbackStrategyOptions.FallbackStrategyOptions() -> void -Polly.Fallback.FallbackStrategyOptions.OnFallback.get -> System.Func, System.Threading.Tasks.ValueTask>? +Polly.Fallback.FallbackStrategyOptions.OnFallback.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.Fallback.FallbackStrategyOptions.OnFallback.set -> void Polly.Fallback.FallbackStrategyOptions.ShouldHandle.get -> System.Func, System.Threading.Tasks.ValueTask>! Polly.Fallback.FallbackStrategyOptions.ShouldHandle.set -> void @@ -117,7 +121,7 @@ Polly.Hedging.HedgingStrategyOptions.HedgingDelayGenerator.set -> void Polly.Hedging.HedgingStrategyOptions.HedgingStrategyOptions() -> void Polly.Hedging.HedgingStrategyOptions.MaxHedgedAttempts.get -> int Polly.Hedging.HedgingStrategyOptions.MaxHedgedAttempts.set -> void -Polly.Hedging.HedgingStrategyOptions.OnHedging.get -> System.Func, System.Threading.Tasks.ValueTask>? +Polly.Hedging.HedgingStrategyOptions.OnHedging.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.Hedging.HedgingStrategyOptions.OnHedging.set -> void Polly.Hedging.HedgingStrategyOptions.ShouldHandle.get -> System.Func, System.Threading.Tasks.ValueTask>! Polly.Hedging.HedgingStrategyOptions.ShouldHandle.set -> void @@ -125,6 +129,7 @@ Polly.Hedging.OnHedgingArguments Polly.Hedging.OnHedgingArguments.AttemptNumber.get -> int Polly.Hedging.OnHedgingArguments.Duration.get -> System.TimeSpan Polly.Hedging.OnHedgingArguments.HasOutcome.get -> bool +Polly.Hedging.OnHedgingArguments.OnHedgingArguments() -> void Polly.Hedging.OnHedgingArguments.OnHedgingArguments(int attemptNumber, bool hasOutcome, System.TimeSpan duration) -> void Polly.HedgingResiliencePipelineBuilderExtensions Polly.LegacySupport @@ -250,12 +255,12 @@ Polly.ResiliencePipelineBuilder Polly.ResiliencePipelineBuilder.Build() -> Polly.ResiliencePipeline! Polly.ResiliencePipelineBuilder.ResiliencePipelineBuilder() -> void Polly.ResiliencePipelineBuilderBase -Polly.ResiliencePipelineBuilderBase.DiagnosticSource.get -> System.Diagnostics.DiagnosticSource? -Polly.ResiliencePipelineBuilderBase.DiagnosticSource.set -> void Polly.ResiliencePipelineBuilderBase.InstanceName.get -> string? Polly.ResiliencePipelineBuilderBase.InstanceName.set -> void Polly.ResiliencePipelineBuilderBase.Name.get -> string? Polly.ResiliencePipelineBuilderBase.Name.set -> void +Polly.ResiliencePipelineBuilderBase.TelemetryListener.get -> Polly.Telemetry.TelemetryListener? +Polly.ResiliencePipelineBuilderBase.TelemetryListener.set -> void Polly.ResiliencePipelineBuilderBase.Validator.get -> System.Action! Polly.ResiliencePipelineBuilderExtensions Polly.ResilienceProperties @@ -283,6 +288,7 @@ Polly.ResilienceValidationContext.ResilienceValidationContext(object! instance, Polly.Retry.OnRetryArguments Polly.Retry.OnRetryArguments.AttemptNumber.get -> int Polly.Retry.OnRetryArguments.ExecutionTime.get -> System.TimeSpan +Polly.Retry.OnRetryArguments.OnRetryArguments() -> void Polly.Retry.OnRetryArguments.OnRetryArguments(int attemptNumber, System.TimeSpan retryDelay, System.TimeSpan executionTime) -> void Polly.Retry.OnRetryArguments.RetryDelay.get -> System.TimeSpan Polly.Retry.RetryBackoffType @@ -305,7 +311,7 @@ Polly.Retry.RetryStrategyOptions.BackoffType.get -> Polly.Retry.RetryBa Polly.Retry.RetryStrategyOptions.BackoffType.set -> void Polly.Retry.RetryStrategyOptions.BaseDelay.get -> System.TimeSpan Polly.Retry.RetryStrategyOptions.BaseDelay.set -> void -Polly.Retry.RetryStrategyOptions.OnRetry.get -> System.Func, System.Threading.Tasks.ValueTask>? +Polly.Retry.RetryStrategyOptions.OnRetry.get -> System.Func, System.Threading.Tasks.ValueTask>? Polly.Retry.RetryStrategyOptions.OnRetry.set -> void Polly.Retry.RetryStrategyOptions.Randomizer.get -> System.Func! Polly.Retry.RetryStrategyOptions.Randomizer.set -> void @@ -327,10 +333,12 @@ Polly.StrategyBuilderContext.Telemetry.get -> Polly.Telemetry.ResilienceStrategy Polly.Telemetry.ExecutionAttemptArguments Polly.Telemetry.ExecutionAttemptArguments.AttemptNumber.get -> int Polly.Telemetry.ExecutionAttemptArguments.Duration.get -> System.TimeSpan +Polly.Telemetry.ExecutionAttemptArguments.ExecutionAttemptArguments() -> void 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() -> void Polly.Telemetry.PipelineExecutedArguments.PipelineExecutedArguments(System.TimeSpan duration) -> void Polly.Telemetry.PipelineExecutingArguments Polly.Telemetry.PipelineExecutingArguments.PipelineExecutingArguments() -> void @@ -355,14 +363,19 @@ Polly.Telemetry.ResilienceTelemetrySource.PipelineInstanceName.get -> string? Polly.Telemetry.ResilienceTelemetrySource.PipelineName.get -> string? Polly.Telemetry.ResilienceTelemetrySource.ResilienceTelemetrySource(string? pipelineName, string? pipelineInstanceName, string? strategyName) -> void Polly.Telemetry.ResilienceTelemetrySource.StrategyName.get -> string? -Polly.Telemetry.TelemetryEventArguments -Polly.Telemetry.TelemetryEventArguments.Arguments.get -> object! -Polly.Telemetry.TelemetryEventArguments.Context.get -> Polly.ResilienceContext! -Polly.Telemetry.TelemetryEventArguments.Event.get -> Polly.Telemetry.ResilienceEvent -Polly.Telemetry.TelemetryEventArguments.Outcome.get -> Polly.Outcome? -Polly.Telemetry.TelemetryEventArguments.Source.get -> Polly.Telemetry.ResilienceTelemetrySource! +Polly.Telemetry.TelemetryEventArguments +Polly.Telemetry.TelemetryEventArguments.Arguments.get -> TArgs +Polly.Telemetry.TelemetryEventArguments.Context.get -> Polly.ResilienceContext! +Polly.Telemetry.TelemetryEventArguments.Event.get -> Polly.Telemetry.ResilienceEvent +Polly.Telemetry.TelemetryEventArguments.Outcome.get -> Polly.Outcome? +Polly.Telemetry.TelemetryEventArguments.Source.get -> Polly.Telemetry.ResilienceTelemetrySource! +Polly.Telemetry.TelemetryEventArguments.TelemetryEventArguments() -> void +Polly.Telemetry.TelemetryEventArguments.TelemetryEventArguments(Polly.Telemetry.ResilienceTelemetrySource! source, Polly.Telemetry.ResilienceEvent resilienceEvent, Polly.ResilienceContext! context, TArgs args, Polly.Outcome? outcome) -> void +Polly.Telemetry.TelemetryListener +Polly.Telemetry.TelemetryListener.TelemetryListener() -> void Polly.Timeout.OnTimeoutArguments Polly.Timeout.OnTimeoutArguments.Context.get -> Polly.ResilienceContext! +Polly.Timeout.OnTimeoutArguments.OnTimeoutArguments() -> void Polly.Timeout.OnTimeoutArguments.OnTimeoutArguments(Polly.ResilienceContext! context, System.TimeSpan timeout) -> void Polly.Timeout.OnTimeoutArguments.Timeout.get -> System.TimeSpan Polly.Timeout.TimeoutGeneratorArguments @@ -378,7 +391,7 @@ Polly.Timeout.TimeoutRejectedException.TimeoutRejectedException(string! message, Polly.Timeout.TimeoutRejectedException.TimeoutRejectedException(string! message, System.TimeSpan timeout, System.Exception! innerException) -> void Polly.Timeout.TimeoutRejectedException.TimeoutRejectedException(System.TimeSpan timeout) -> void Polly.Timeout.TimeoutStrategyOptions -Polly.Timeout.TimeoutStrategyOptions.OnTimeout.get -> System.Func? +Polly.Timeout.TimeoutStrategyOptions.OnTimeout.get -> System.Func? Polly.Timeout.TimeoutStrategyOptions.OnTimeout.set -> void Polly.Timeout.TimeoutStrategyOptions.Timeout.get -> System.TimeSpan Polly.Timeout.TimeoutStrategyOptions.Timeout.set -> void diff --git a/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs b/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs index 9366c4ff4de..09da82441be 100644 --- a/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs +++ b/src/Polly.Core/Registry/ResiliencePipelineRegistry.cs @@ -282,7 +282,7 @@ private static ResiliencePipeline CreatePipeline( var builder = factory(); var pipeline = builder.BuildPipeline(); - var diagnosticSource = builder.DiagnosticSource; + var diagnosticSource = builder.TelemetryListener; if (context.ReloadTokenProducer is null) { diff --git a/src/Polly.Core/ResiliencePipelineBuilderBase.cs b/src/Polly.Core/ResiliencePipelineBuilderBase.cs index f7a16e92b06..8f1ae2d2ca2 100644 --- a/src/Polly.Core/ResiliencePipelineBuilderBase.cs +++ b/src/Polly.Core/ResiliencePipelineBuilderBase.cs @@ -27,7 +27,7 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot { Name = other.Name; TimeProvider = other.TimeProvider; - DiagnosticSource = other.DiagnosticSource; + TelemetryListener = other.TelemetryListener; } /// @@ -66,7 +66,7 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot internal TimeProvider TimeProvider { get; set; } = TimeProvider.System; /// - /// Gets or sets the that is used by Polly to report resilience events. + /// Gets or sets the that is used by Polly to report resilience events. /// /// /// This property is used by the telemetry infrastructure and should not be used directly by user code. @@ -75,7 +75,7 @@ private protected ResiliencePipelineBuilderBase(ResiliencePipelineBuilderBase ot /// The default value is . /// [EditorBrowsable(EditorBrowsableState.Never)] - public DiagnosticSource? DiagnosticSource { get; set; } + public TelemetryListener? TelemetryListener { get; set; } /// /// Gets the validator that is used for the validation. @@ -119,7 +119,7 @@ internal ResiliencePipeline BuildPipeline() return CompositeResiliencePipeline.Create( strategies, - TelemetryUtil.CreateTelemetry(DiagnosticSource, Name, InstanceName, null), + TelemetryUtil.CreateTelemetry(TelemetryListener, Name, InstanceName, null), TimeProvider); } @@ -130,7 +130,7 @@ private ResiliencePipeline CreateResiliencePipeline(Entry entry) builderInstanceName: InstanceName, strategyName: entry.Options.Name, timeProvider: TimeProvider, - diagnosticSource: DiagnosticSource); + telemetryListener: TelemetryListener); var strategy = entry.Factory(context); strategy.Options = entry.Options; diff --git a/src/Polly.Core/Retry/OnRetryArguments.cs b/src/Polly.Core/Retry/OnRetryArguments.cs index 9edbf14e2a3..f195e739d81 100644 --- a/src/Polly.Core/Retry/OnRetryArguments.cs +++ b/src/Polly.Core/Retry/OnRetryArguments.cs @@ -1,12 +1,14 @@ namespace Polly.Retry; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Represents the arguments used by for handling the retry event. /// -public sealed class OnRetryArguments +public readonly struct OnRetryArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The zero-based attempt number. /// The delay before the next retry. diff --git a/src/Polly.Core/StrategyBuilderContext.cs b/src/Polly.Core/StrategyBuilderContext.cs index 7edb9cbd3f2..413aa700a6f 100644 --- a/src/Polly.Core/StrategyBuilderContext.cs +++ b/src/Polly.Core/StrategyBuilderContext.cs @@ -12,13 +12,13 @@ internal StrategyBuilderContext( string? builderInstanceName, string? strategyName, TimeProvider timeProvider, - DiagnosticSource? diagnosticSource) + TelemetryListener? telemetryListener) { BuilderName = builderName; BuilderInstanceName = builderInstanceName; StrategyName = strategyName; TimeProvider = timeProvider; - Telemetry = TelemetryUtil.CreateTelemetry(diagnosticSource, builderName, builderInstanceName, strategyName); + Telemetry = TelemetryUtil.CreateTelemetry(telemetryListener, builderName, builderInstanceName, strategyName); } /// diff --git a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs deleted file mode 100644 index 752fd7f277e..00000000000 --- a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.Pool.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Polly.Telemetry; - -public partial class ExecutionAttemptArguments -{ - private static readonly ObjectPool Pool = new(() => new ExecutionAttemptArguments(), args => - { - args.Duration = TimeSpan.Zero; - args.AttemptNumber = 0; - args.Handled = false; - }); - - internal static ExecutionAttemptArguments Get(int attempt, TimeSpan duration, bool handled) - { - var args = Pool.Get(); - args.AttemptNumber = attempt; - args.Duration = duration; - args.Handled = handled; - return args; - } - - internal static void Return(ExecutionAttemptArguments args) => Pool.Return(args); -} diff --git a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs index 2126e1fa302..ecd044eb467 100644 --- a/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs +++ b/src/Polly.Core/Telemetry/ExecutionAttemptArguments.cs @@ -1,12 +1,14 @@ namespace Polly.Telemetry; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments that encapsulate the execution attempt for retries or hedging. /// -public sealed partial class ExecutionAttemptArguments +public readonly struct ExecutionAttemptArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The execution attempt number. /// The execution duration. @@ -18,22 +20,18 @@ public ExecutionAttemptArguments(int attemptNumber, TimeSpan duration, bool hand Handled = handled; } - private ExecutionAttemptArguments() - { - } - /// /// Gets the attempt number. /// - public int AttemptNumber { get; private set; } + public int AttemptNumber { get; } /// /// Gets the execution duration of the attempt. /// - public TimeSpan Duration { get; private set; } + public TimeSpan Duration { get; } /// /// Gets a value indicating whether the outcome was handled by retry or hedging strategy. /// - public bool Handled { get; private set; } + public bool Handled { get; } } diff --git a/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs b/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs deleted file mode 100644 index e71018369dc..00000000000 --- a/src/Polly.Core/Telemetry/PipelineExecutedArguments.Pool.cs +++ /dev/null @@ -1,18 +0,0 @@ -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 index a49c8b504fd..c69da475fd1 100644 --- a/src/Polly.Core/Telemetry/PipelineExecutedArguments.cs +++ b/src/Polly.Core/Telemetry/PipelineExecutedArguments.cs @@ -1,22 +1,20 @@ namespace Polly.Telemetry; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments that indicate the pipeline execution started. /// -public sealed partial class PipelineExecutedArguments +public readonly struct PipelineExecutedArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The pipeline execution duration. public PipelineExecutedArguments(TimeSpan duration) => Duration = duration; - internal PipelineExecutedArguments() - { - } - /// /// Gets the pipeline execution duration. /// - public TimeSpan Duration { get; internal set; } + public TimeSpan Duration { get; } } diff --git a/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs b/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs index de6f4cc83fa..e3c24c17621 100644 --- a/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs +++ b/src/Polly.Core/Telemetry/PipelineExecutingArguments.cs @@ -3,7 +3,6 @@ /// /// Arguments that indicate the pipeline execution started. /// -public sealed class PipelineExecutingArguments +public readonly struct PipelineExecutingArguments { - internal static readonly PipelineExecutingArguments Instance = new(); } diff --git a/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs b/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs index e16fa9885c7..be80cdb2784 100644 --- a/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs +++ b/src/Polly.Core/Telemetry/ResilienceStrategyTelemetry.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - namespace Polly.Telemetry; /// @@ -10,20 +8,20 @@ namespace Polly.Telemetry; /// public sealed class ResilienceStrategyTelemetry { - internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, DiagnosticSource? diagnosticSource) + internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, TelemetryListener? listener) { TelemetrySource = source; - DiagnosticSource = diagnosticSource; + Listener = listener; } - internal DiagnosticSource? DiagnosticSource { get; } + internal TelemetryListener? Listener { get; } internal ResilienceTelemetrySource TelemetrySource { get; } /// /// Gets a value indicating whether telemetry is enabled. /// - public bool IsEnabled => DiagnosticSource is not null; + public bool IsEnabled => Listener is not null; /// /// Reports an event that occurred in a resilience strategy. @@ -33,30 +31,18 @@ internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, Diagnosti /// The resilience context associated with this event. /// The event arguments. /// Thrown when is . - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Reflection is not used when consuming the event.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = "Reflection is not used when consuming the event.")] public void Report(ResilienceEvent resilienceEvent, ResilienceContext context, TArgs args) { Guard.NotNull(context); context.AddResilienceEvent(resilienceEvent); - if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(resilienceEvent.EventName) || resilienceEvent.Severity == ResilienceEventSeverity.None) + if (Listener is null || resilienceEvent.Severity == ResilienceEventSeverity.None) { return; } - var telemetryArgs = TelemetryEventArguments.Get(TelemetrySource, resilienceEvent, context, null, args!); - - DiagnosticSource.Write(resilienceEvent.EventName, telemetryArgs); - - TelemetryEventArguments.Return(telemetryArgs); + Listener.Write(new(TelemetrySource, resilienceEvent, context, args, null)); } /// @@ -66,28 +52,16 @@ public void Report(ResilienceEvent resilienceEvent, ResilienceContext con /// The type of the result. /// The reported resilience event. /// The event arguments. - [UnconditionalSuppressMessage( - "Trimming", - "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", - Justification = "Reflection is not used when consuming the event.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", - Justification = "Reflection is not used when consuming the event.")] public void Report(ResilienceEvent resilienceEvent, OutcomeArguments args) { args.Context.AddResilienceEvent(resilienceEvent); - if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(resilienceEvent.EventName) || resilienceEvent.Severity == ResilienceEventSeverity.None) + if (Listener is null || resilienceEvent.Severity == ResilienceEventSeverity.None) { return; } - var telemetryArgs = TelemetryEventArguments.Get(TelemetrySource, resilienceEvent, args.Context, args.Outcome.AsOutcome(), args.Arguments!); - - DiagnosticSource.Write(resilienceEvent.EventName, telemetryArgs); - - TelemetryEventArguments.Return(telemetryArgs); + Listener.Write(new(TelemetrySource, resilienceEvent, args.Context, args.Arguments, args.Outcome)); } } diff --git a/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs b/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs deleted file mode 100644 index 02b70b9143d..00000000000 --- a/src/Polly.Core/Telemetry/TelemetryEventArguments.Pool.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Polly.Telemetry; - -public sealed partial class TelemetryEventArguments -{ - private static readonly ObjectPool Pool = new(() => new TelemetryEventArguments(), args => - { - args.Source = null!; - args.Event = default; - args.Context = null!; - args.Outcome = default; - args.Arguments = null!; - }); - - internal static TelemetryEventArguments Get( - ResilienceTelemetrySource source, - ResilienceEvent resilienceEvent, - ResilienceContext context, - Outcome? outcome, - object arguments) - { - var args = Pool.Get(); - - args.Source = source; - args.Event = resilienceEvent; - args.Context = context; - args.Outcome = outcome; - args.Arguments = arguments; - - return args; - } - - internal static void Return(TelemetryEventArguments args) => Pool.Return(args); -} diff --git a/src/Polly.Core/Telemetry/TelemetryEventArguments.cs b/src/Polly.Core/Telemetry/TelemetryEventArguments.cs index c450b13f6e2..9b4475f8481 100644 --- a/src/Polly.Core/Telemetry/TelemetryEventArguments.cs +++ b/src/Polly.Core/Telemetry/TelemetryEventArguments.cs @@ -1,36 +1,53 @@ -namespace Polly.Telemetry; +namespace Polly.Telemetry; + +#pragma warning disable CA1815 // Override equals and operator equals on value types /// -/// The arguments of the telemetry event. +/// Represents the information about the resilience event. /// -public sealed partial class TelemetryEventArguments +/// The type of result. +/// The arguments associated with the resilience event. +public readonly struct TelemetryEventArguments { - private TelemetryEventArguments() + /// + /// Initializes a new instance of the struct. + /// + /// The source that produced the resilience event. + /// The resilience event. + /// The context associated with the resilience event. + /// The arguments associated with the resilience event. + /// The outcome associated with the resilience event, if any. + public TelemetryEventArguments(ResilienceTelemetrySource source, ResilienceEvent resilienceEvent, ResilienceContext context, TArgs args, Outcome? outcome) { + Source = source; + Event = resilienceEvent; + Context = context; + Arguments = args; + Outcome = outcome; } /// - /// Gets the source of the event. + /// Gets the source that produced the resilience event. /// - public ResilienceTelemetrySource Source { get; private set; } = null!; + public ResilienceTelemetrySource Source { get; } /// - /// Gets the event. + /// Gets the resilience event. /// - public ResilienceEvent Event { get; private set; } + public ResilienceEvent Event { get; } /// - /// Gets the resilience context. + /// Gets the context associated with the resilience event. /// - public ResilienceContext Context { get; private set; } = null!; + public ResilienceContext Context { get; } /// - /// Gets the outcome of an execution. + /// Gets the arguments associated with the resilience event. /// - public Outcome? Outcome { get; private set; } + public TArgs Arguments { get; } /// - /// Gets the arguments associated with the event. + /// Gets the outcome associated with the resilience event, if any. /// - public object Arguments { get; private set; } = null!; + public Outcome? Outcome { get; } } diff --git a/src/Polly.Core/Telemetry/TelemetryListener.cs b/src/Polly.Core/Telemetry/TelemetryListener.cs new file mode 100644 index 00000000000..189a8cd2544 --- /dev/null +++ b/src/Polly.Core/Telemetry/TelemetryListener.cs @@ -0,0 +1,15 @@ +namespace Polly.Telemetry; + +/// +/// Listener of resilience telemetry events. +/// +public abstract class TelemetryListener +{ + /// + /// Writes a resilience event to the listener. + /// + /// The result type. + /// The type of arguments associated with the event. + /// The arguments associated with the event. + public abstract void Write(in TelemetryEventArguments args); +} diff --git a/src/Polly.Core/Telemetry/TelemetryUtil.cs b/src/Polly.Core/Telemetry/TelemetryUtil.cs index 2a140f447e5..fca56924449 100644 --- a/src/Polly.Core/Telemetry/TelemetryUtil.cs +++ b/src/Polly.Core/Telemetry/TelemetryUtil.cs @@ -11,14 +11,14 @@ internal static class TelemetryUtil internal const string PipelineExecuted = "PipelineExecuted"; public static ResilienceStrategyTelemetry CreateTelemetry( - DiagnosticSource? diagnosticSource, + TelemetryListener? listener, string? builderName, string? builderInstanceName, string? strategyName) { var telemetrySource = new ResilienceTelemetrySource(builderName, builderInstanceName, strategyName); - return new ResilienceStrategyTelemetry(telemetrySource, diagnosticSource); + return new ResilienceStrategyTelemetry(telemetrySource, listener); } public static void ReportExecutionAttempt( @@ -34,10 +34,8 @@ public static void ReportExecutionAttempt( return; } - var attemptArgs = ExecutionAttemptArguments.Get(attempt, executionTime, handled); telemetry.Report( new(handled ? ResilienceEventSeverity.Warning : ResilienceEventSeverity.Information, ExecutionAttempt), - new(context, outcome, attemptArgs)); - ExecutionAttemptArguments.Return(attemptArgs); + new(context, outcome, new ExecutionAttemptArguments(attempt, executionTime, handled))); } } diff --git a/src/Polly.Core/Timeout/OnTimeoutArguments.cs b/src/Polly.Core/Timeout/OnTimeoutArguments.cs index b235bf724a9..3f1fd6f96a0 100644 --- a/src/Polly.Core/Timeout/OnTimeoutArguments.cs +++ b/src/Polly.Core/Timeout/OnTimeoutArguments.cs @@ -1,12 +1,14 @@ namespace Polly.Timeout; +#pragma warning disable CA1815 // Override equals and operator equals on value types + /// /// Arguments used by the timeout strategy to notify that a timeout occurred. /// -public sealed class OnTimeoutArguments +public readonly struct OnTimeoutArguments { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the struct. /// /// The context associated with the execution of a user-provided callback. /// The timeout value assigned. diff --git a/src/Polly.Core/Utils/CompositeResiliencePipeline.cs b/src/Polly.Core/Utils/CompositeResiliencePipeline.cs index 15a0cc51e4e..cfd1219eb8a 100644 --- a/src/Polly.Core/Utils/CompositeResiliencePipeline.cs +++ b/src/Polly.Core/Utils/CompositeResiliencePipeline.cs @@ -73,7 +73,7 @@ internal override async ValueTask> ExecuteCore TState state) { var timeStamp = _timeProvider.GetTimestamp(); - _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Debug, TelemetryUtil.PipelineExecuting), context, PipelineExecutingArguments.Instance); + _telemetry.Report(new ResilienceEvent(ResilienceEventSeverity.Debug, TelemetryUtil.PipelineExecuting), context, default(PipelineExecutingArguments)); Outcome outcome; @@ -86,11 +86,12 @@ internal override async ValueTask> ExecuteCore 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); + new OutcomeArguments( + context, + outcome, + new PipelineExecutedArguments(_timeProvider.GetElapsedTime(timeStamp)))); return outcome; } diff --git a/src/Polly.Extensions/PublicAPI.Unshipped.txt b/src/Polly.Extensions/PublicAPI.Unshipped.txt index 1431166ab73..506c18d29dd 100644 --- a/src/Polly.Extensions/PublicAPI.Unshipped.txt +++ b/src/Polly.Extensions/PublicAPI.Unshipped.txt @@ -1,25 +1,28 @@ #nullable enable +abstract Polly.Telemetry.MeteringEnricher.Enrich(in Polly.Telemetry.EnrichmentContext context) -> void Polly.DependencyInjection.AddResiliencePipelineContext Polly.DependencyInjection.AddResiliencePipelineContext.BuilderName.get -> string! Polly.DependencyInjection.AddResiliencePipelineContext.EnableReloads(string? name = null) -> void Polly.DependencyInjection.AddResiliencePipelineContext.GetOptions(string? name = null) -> TOptions -Polly.DependencyInjection.AddResiliencePipelineContext.ServiceProvider.get -> System.IServiceProvider! Polly.DependencyInjection.AddResiliencePipelineContext.PipelineKey.get -> TKey +Polly.DependencyInjection.AddResiliencePipelineContext.ServiceProvider.get -> System.IServiceProvider! Polly.PollyServiceCollectionExtensions Polly.Registry.ConfigureBuilderContextExtensions -Polly.Telemetry.EnrichmentContext -Polly.Telemetry.EnrichmentContext.Arguments.get -> object? -Polly.Telemetry.EnrichmentContext.Context.get -> Polly.ResilienceContext! -Polly.Telemetry.EnrichmentContext.Outcome.get -> Polly.Outcome? -Polly.Telemetry.EnrichmentContext.Tags.get -> System.Collections.Generic.IList>! +Polly.Telemetry.EnrichmentContext +Polly.Telemetry.EnrichmentContext.EnrichmentContext() -> void +Polly.Telemetry.EnrichmentContext.EnrichmentContext(in Polly.Telemetry.TelemetryEventArguments telemetryEvent, System.Collections.Generic.IList>! tags) -> void +Polly.Telemetry.EnrichmentContext.Tags.get -> System.Collections.Generic.IList>! +Polly.Telemetry.EnrichmentContext.TelemetryEvent.get -> Polly.Telemetry.TelemetryEventArguments +Polly.Telemetry.MeteringEnricher +Polly.Telemetry.MeteringEnricher.MeteringEnricher() -> void Polly.Telemetry.TelemetryOptions -Polly.Telemetry.TelemetryOptions.Enrichers.get -> System.Collections.Generic.ICollection!>! Polly.Telemetry.TelemetryOptions.LoggerFactory.get -> Microsoft.Extensions.Logging.ILoggerFactory! Polly.Telemetry.TelemetryOptions.LoggerFactory.set -> void -Polly.Telemetry.TelemetryOptions.OnTelemetryEvent.get -> System.Action? -Polly.Telemetry.TelemetryOptions.OnTelemetryEvent.set -> void +Polly.Telemetry.TelemetryOptions.MeteringEnrichers.get -> System.Collections.Generic.ICollection! Polly.Telemetry.TelemetryOptions.ResultFormatter.get -> System.Func! Polly.Telemetry.TelemetryOptions.ResultFormatter.set -> void +Polly.Telemetry.TelemetryOptions.TelemetryListener.get -> Polly.Telemetry.TelemetryListener? +Polly.Telemetry.TelemetryOptions.TelemetryListener.set -> void Polly.Telemetry.TelemetryOptions.TelemetryOptions() -> void Polly.TelemetryResiliencePipelineBuilderExtensions static Polly.PollyServiceCollectionExtensions.AddResiliencePipeline(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, TKey key, System.Action!, Polly.DependencyInjection.AddResiliencePipelineContext!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs b/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs deleted file mode 100644 index f614249f933..00000000000 --- a/src/Polly.Extensions/Telemetry/EnrichmentContext.Pool.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Polly.Utils; - -namespace Polly.Telemetry; - -public partial class EnrichmentContext -{ - private static readonly ObjectPool ContextPool = new( - static () => new EnrichmentContext(), - static context => - { - context.Outcome = null; - context.Context = null!; - context.Tags.Clear(); - return true; - }); - - internal static EnrichmentContext Get(ResilienceContext resilienceContext, object? arguments, Outcome? outcome) - { - var context = ContextPool.Get(); - context.Context = resilienceContext; - context.Arguments = arguments; - context.Outcome = outcome; - - return context; - } - - internal static void Return(EnrichmentContext context) - { - Array.Clear(context._tagsArray, 0, context.Tags.Count); - context.Tags.Clear(); - ContextPool.Return(context); - } -} diff --git a/src/Polly.Extensions/Telemetry/EnrichmentContext.cs b/src/Polly.Extensions/Telemetry/EnrichmentContext.cs index aee94a8e20b..6d6580d5ebb 100644 --- a/src/Polly.Extensions/Telemetry/EnrichmentContext.cs +++ b/src/Polly.Extensions/Telemetry/EnrichmentContext.cs @@ -1,54 +1,38 @@ -namespace Polly.Telemetry; +namespace Polly.Telemetry; + +#pragma warning disable CA1815 // Override equals and operator equals on value types /// -/// Enrichment context used when reporting resilience telemetry. This context is passed to the registered enrichers in . +/// Enrichment context used when reporting resilience events. /// -public sealed partial class EnrichmentContext +/// The type of the result. +/// The type of the arguments attached to the resilience event. +/// +/// This context is passed to enrichers in . +/// +public readonly struct EnrichmentContext { - private const int InitialArraySize = 20; - - private KeyValuePair[] _tagsArray = new KeyValuePair[InitialArraySize]; - - private EnrichmentContext() - { - } - - /// - /// Gets the outcome of the operation if any. - /// - public Outcome? Outcome { get; internal set; } - /// - /// Gets the resilience arguments associated with the resilience event, if any. + /// Initializes a new instance of the struct. /// - public object? Arguments { get; internal set; } + /// The telemetry event info. + /// Tags associated with the resilience event. + public EnrichmentContext(in TelemetryEventArguments telemetryEvent, IList> tags) + { + TelemetryEvent = telemetryEvent; + Tags = tags; + } /// - /// Gets the resilience context associated with the operation that produced the resilience event. + /// Gets the info about the telemetry event. /// - public ResilienceContext Context { get; internal set; } = null!; + public TelemetryEventArguments TelemetryEvent { get; } /// /// Gets the tags associated with the resilience event. /// - public IList> Tags { get; } = new List>(); - - internal ReadOnlySpan> TagsSpan - { - get - { - // stryker disable once equality : no means to test this - if (Tags.Count > _tagsArray.Length) - { - Array.Resize(ref _tagsArray, Tags.Count); - } - - for (int i = 0; i < Tags.Count; i++) - { - _tagsArray[i] = Tags[i]; - } - - return _tagsArray.AsSpan(0, Tags.Count); - } - } + /// + /// Custom enricher can add tags to this collection. + /// + public IList> Tags { get; } } diff --git a/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs b/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs deleted file mode 100644 index 3d269f57d2c..00000000000 --- a/src/Polly.Extensions/Telemetry/EnrichmentUtil.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Polly.Telemetry; - -internal static class EnrichmentUtil -{ - public static void Enrich(EnrichmentContext context, List> enrichers) - { - if (enrichers.Count == 0) - { - return; - } - - foreach (var enricher in enrichers) - { - enricher(context); - } - } -} diff --git a/src/Polly.Extensions/Telemetry/MeteringEnricher.cs b/src/Polly.Extensions/Telemetry/MeteringEnricher.cs new file mode 100644 index 00000000000..fccf4cd9a3e --- /dev/null +++ b/src/Polly.Extensions/Telemetry/MeteringEnricher.cs @@ -0,0 +1,15 @@ +namespace Polly.Telemetry; + +/// +/// Enricher used to enrich the metrics with additional information. +/// +public abstract class MeteringEnricher +{ + /// + /// Enriches the metrics with additional information. + /// + /// The type of result. + /// The type of arguments. + /// The enrichment context. + public abstract void Enrich(in EnrichmentContext context); +} diff --git a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs b/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs deleted file mode 100644 index 8584647da1b..00000000000 --- a/src/Polly.Extensions/Telemetry/ResilienceTelemetryDiagnosticSource.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System.Diagnostics.Metrics; -using Microsoft.Extensions.Logging; - -namespace Polly.Telemetry; - -internal sealed class ResilienceTelemetryDiagnosticSource : DiagnosticSource -{ - internal static readonly Meter Meter = new(TelemetryUtil.PollyDiagnosticSource, "1.0"); - - private readonly ILogger _logger; - private readonly Func _resultFormatter; - private readonly Action? _onEvent; - private readonly List> _enrichers; - - public ResilienceTelemetryDiagnosticSource(TelemetryOptions options) - { - _enrichers = options.Enrichers.ToList(); - _logger = options.LoggerFactory.CreateLogger(TelemetryUtil.PollyDiagnosticSource); - _resultFormatter = options.ResultFormatter; - _onEvent = options.OnTelemetryEvent; - - Counter = Meter.CreateCounter( - "resilience-events", - description: "Tracks the number of resilience events that occurred in resilience strategies."); - - AttemptDuration = Meter.CreateHistogram( - "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. -#pragma warning disable IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. - public override void Write(string name, object? value) -#pragma warning restore IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. -#pragma warning restore IL2046 // 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. - { - if (value is not TelemetryEventArguments args) - { - return; - } - - _onEvent?.Invoke(args); - - LogEvent(args); - MeterEvent(args); - } - - private static void AddCommonTags(TelemetryEventArguments args, ResilienceTelemetrySource source, EnrichmentContext enrichmentContext) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.EventName, args.Event.EventName)); - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.EventSeverity, args.Event.Severity.AsString())); - - if (source.PipelineName is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.PipelineName, source.PipelineName)); - } - - if (source.PipelineInstanceName is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.PipelineInstance, source.PipelineInstanceName)); - } - - if (source.StrategyName is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.StrategyName, source.StrategyName)); - } - - if (enrichmentContext.Context.OperationKey is not null) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.OperationKey, enrichmentContext.Context.OperationKey)); - } - - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ResultType, args.Context.GetResultType())); - - if (args.Outcome?.Exception is Exception e) - { - enrichmentContext.Tags.Add(new(ResilienceTelemetryTags.ExceptionName, e.GetType().FullName)); - } - } - - private void MeterEvent(TelemetryEventArguments args) - { - var source = args.Source; - - 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) - { - return; - } - - var enrichmentContext = EnrichmentContext.Get(args.Context, args.Arguments, args.Outcome); - AddCommonTags(args, source, enrichmentContext); - 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.Duration.TotalMilliseconds, enrichmentContext.TagsSpan); - EnrichmentContext.Return(enrichmentContext); - } - else if (Counter.Enabled) - { - var enrichmentContext = EnrichmentContext.Get(args.Context, args.Arguments, args.Outcome); - AddCommonTags(args, source, enrichmentContext); - EnrichmentUtil.Enrich(enrichmentContext, _enrichers); - Counter.Add(1, enrichmentContext.TagsSpan); - EnrichmentContext.Return(enrichmentContext); - } - } - - private void LogEvent(TelemetryEventArguments args) - { - var result = args.Outcome?.Result; - if (result is not null) - { - result = _resultFormatter(args.Context, result); - } - else if (args.Outcome?.Exception is Exception e) - { - result = e.Message; - } - - var level = args.Event.Severity.AsLogLevel(); - - if (args.Arguments is PipelineExecutingArguments pipelineExecutionStarted) - { - _logger.PipelineExecuting( - args.Source.PipelineName.GetValueOrPlaceholder(), - args.Source.PipelineInstanceName.GetValueOrPlaceholder(), - args.Context.OperationKey, - args.Context.GetResultType()); - } - else if (args.Arguments is PipelineExecutedArguments pipelineExecutionFinished) - { - var logLevel = args.Context.IsExecutionHealthy() ? LogLevel.Debug : LogLevel.Warning; - - _logger.PipelineExecuted( - logLevel, - args.Source.PipelineName.GetValueOrPlaceholder(), - args.Source.PipelineInstanceName.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)) - { - _logger.ExecutionAttempt( - level, - args.Source.PipelineName.GetValueOrPlaceholder(), - args.Source.PipelineInstanceName.GetValueOrPlaceholder(), - args.Source.StrategyName.GetValueOrPlaceholder(), - args.Context.OperationKey, - result, - executionAttempt.Handled, - executionAttempt.AttemptNumber, - executionAttempt.Duration.TotalMilliseconds, - args.Outcome?.Exception); - } - } - else - { - _logger.ResilienceEvent( - level, - args.Event.EventName, - args.Source.PipelineName.GetValueOrPlaceholder(), - args.Source.PipelineInstanceName.GetValueOrPlaceholder(), - args.Source.StrategyName.GetValueOrPlaceholder(), - args.Context.OperationKey, - result, - 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/TagsList.cs b/src/Polly.Extensions/Telemetry/TagsList.cs new file mode 100644 index 00000000000..17a9bc00fc4 --- /dev/null +++ b/src/Polly.Extensions/Telemetry/TagsList.cs @@ -0,0 +1,58 @@ +using Polly.Utils; + +namespace Polly.Telemetry; + +internal sealed class TagsList +{ + private const int InitialArraySize = 20; + + private static readonly ObjectPool ContextPool = new(static () => new TagsList(), static context => + { + context.Tags.Clear(); + return true; + }); + + private KeyValuePair[] _tagsArray = new KeyValuePair[InitialArraySize]; + + private TagsList() + { + } + + internal static TagsList Get() + { + var context = ContextPool.Get(); + + return context; + } + + internal static void Return(TagsList context) + { + Array.Clear(context._tagsArray, 0, context.Tags.Count); + context.Tags.Clear(); + ContextPool.Return(context); + } + + /// + /// Gets the tags associated with the resilience event. + /// + public IList> Tags { get; } = new List>(); + + internal ReadOnlySpan> TagsSpan + { + get + { + // stryker disable once equality : no means to test this + if (Tags.Count > _tagsArray.Length) + { + Array.Resize(ref _tagsArray, Tags.Count); + } + + for (int i = 0; i < Tags.Count; i++) + { + _tagsArray[i] = Tags[i]; + } + + return _tagsArray.AsSpan(0, Tags.Count); + } + } +} diff --git a/src/Polly.Extensions/Telemetry/TelemetryListenerImpl.cs b/src/Polly.Extensions/Telemetry/TelemetryListenerImpl.cs new file mode 100644 index 00000000000..8fd96714134 --- /dev/null +++ b/src/Polly.Extensions/Telemetry/TelemetryListenerImpl.cs @@ -0,0 +1,229 @@ +using System.Diagnostics.Metrics; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; + +namespace Polly.Telemetry; + +internal sealed class TelemetryListenerImpl : TelemetryListener +{ + internal static readonly Meter Meter = new(TelemetryUtil.PollyDiagnosticSource, "1.0"); + + private readonly ILogger _logger; + private readonly Func _resultFormatter; + private readonly List _enrichers; + private readonly TelemetryListener? _listener; + + public TelemetryListenerImpl(TelemetryOptions options) + { + _enrichers = options.MeteringEnrichers.ToList(); + _logger = options.LoggerFactory.CreateLogger(TelemetryUtil.PollyDiagnosticSource); + _resultFormatter = options.ResultFormatter; + _listener = options.TelemetryListener; + + Counter = Meter.CreateCounter( + "resilience-events", + description: "Tracks the number of resilience events that occurred in resilience strategies."); + + AttemptDuration = Meter.CreateHistogram( + "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 void Write(in TelemetryEventArguments args) + { + _listener?.Write(in args); + + LogEvent(in args); + MeterEvent(in args); + } + + private static bool GetArgs(T inArgs, out TArgs outArgs) + { + if (typeof(T) == typeof(TArgs)) + { + outArgs = Unsafe.As(ref inArgs); + return true; + } + + outArgs = default!; + return false; + } + + private static void AddCommonTags(in EnrichmentContext context) + { + var source = context.TelemetryEvent.Source; + var ev = context.TelemetryEvent.Event; + + context.Tags.Add(new(ResilienceTelemetryTags.EventName, context.TelemetryEvent.Event.EventName)); + context.Tags.Add(new(ResilienceTelemetryTags.EventSeverity, context.TelemetryEvent.Event.Severity.AsString())); + + if (source.PipelineName is not null) + { + context.Tags.Add(new(ResilienceTelemetryTags.PipelineName, source.PipelineName)); + } + + if (source.PipelineInstanceName is not null) + { + context.Tags.Add(new(ResilienceTelemetryTags.PipelineInstance, source.PipelineInstanceName)); + } + + if (source.StrategyName is not null) + { + context.Tags.Add(new(ResilienceTelemetryTags.StrategyName, source.StrategyName)); + } + + if (context.TelemetryEvent.Context.OperationKey is not null) + { + context.Tags.Add(new(ResilienceTelemetryTags.OperationKey, context.TelemetryEvent.Context.OperationKey)); + } + + context.Tags.Add(new(ResilienceTelemetryTags.ResultType, context.TelemetryEvent.Context.GetResultType())); + + if (context.TelemetryEvent.Outcome?.Exception is Exception e) + { + context.Tags.Add(new(ResilienceTelemetryTags.ExceptionName, e.GetType().FullName)); + } + } + + private void MeterEvent(in TelemetryEventArguments args) + { + var arguments = args.Arguments; + + if (GetArgs(args.Arguments, out var executionFinished)) + { + if (!ExecutionDuration.Enabled) + { + return; + } + + var tags = TagsList.Get(); + var context = new EnrichmentContext(in args, tags.Tags); + UpdateEnrichmentContext(in context); + tags.Tags.Add(new(ResilienceTelemetryTags.ExecutionHealth, args.Context.GetExecutionHealth())); + ExecutionDuration.Record(executionFinished.Duration.TotalMilliseconds, tags.TagsSpan); + TagsList.Return(tags); + } + else if (GetArgs(args.Arguments, out var executionAttempt)) + { + if (!AttemptDuration.Enabled) + { + return; + } + + var tags = TagsList.Get(); + var context = new EnrichmentContext(in args, tags.Tags); + UpdateEnrichmentContext(in context); + context.Tags.Add(new(ResilienceTelemetryTags.AttemptNumber, executionAttempt.AttemptNumber.AsBoxedInt())); + context.Tags.Add(new(ResilienceTelemetryTags.AttemptHandled, executionAttempt.Handled.AsBoxedBool())); + AttemptDuration.Record(executionAttempt.Duration.TotalMilliseconds, tags.TagsSpan); + TagsList.Return(tags); + } + else if (Counter.Enabled) + { + var tags = TagsList.Get(); + var context = new EnrichmentContext(in args, tags.Tags); + UpdateEnrichmentContext(in context); + Counter.Add(1, tags.TagsSpan); + TagsList.Return(tags); + } + } + + private void UpdateEnrichmentContext(in EnrichmentContext context) + { + AddCommonTags(in context); + + if (_enrichers.Count != 0) + { + foreach (var enricher in _enrichers) + { + enricher.Enrich(in context); + } + } + } + + private void LogEvent(in TelemetryEventArguments args) + { + var result = GetResult(args.Context, args.Outcome); + var level = args.Event.Severity.AsLogLevel(); + + if (GetArgs(args.Arguments, out _)) + { + _logger.PipelineExecuting( + args.Source.PipelineName.GetValueOrPlaceholder(), + args.Source.PipelineInstanceName.GetValueOrPlaceholder(), + args.Context.OperationKey, + args.Context.GetResultType()); + } + else if (GetArgs(args.Arguments, out var pipelineExecuted)) + { + var logLevel = args.Context.IsExecutionHealthy() ? LogLevel.Debug : LogLevel.Warning; + + _logger.PipelineExecuted( + logLevel, + args.Source.PipelineName.GetValueOrPlaceholder(), + args.Source.PipelineInstanceName.GetValueOrPlaceholder(), + args.Context.OperationKey, + args.Context.GetResultType(), + GetResult(args.Context, args.Outcome), + args.Context.GetExecutionHealth(), + pipelineExecuted.Duration.TotalMilliseconds, + args.Outcome?.Exception); + } + else if (GetArgs(args.Arguments, out var executionAttempt)) + { + if (_logger.IsEnabled(level)) + { + _logger.ExecutionAttempt( + level, + args.Source.PipelineName.GetValueOrPlaceholder(), + args.Source.PipelineInstanceName.GetValueOrPlaceholder(), + args.Source.StrategyName.GetValueOrPlaceholder(), + args.Context.OperationKey, + result, + executionAttempt.Handled, + executionAttempt.AttemptNumber, + executionAttempt.Duration.TotalMilliseconds, + args.Outcome?.Exception); + } + } + else + { + _logger.ResilienceEvent( + level, + args.Event.EventName, + args.Source.PipelineName.GetValueOrPlaceholder(), + args.Source.PipelineInstanceName.GetValueOrPlaceholder(), + args.Source.StrategyName.GetValueOrPlaceholder(), + args.Context.OperationKey, + result, + args.Outcome?.Exception); + } + } + + private object? GetResult(ResilienceContext context, Outcome? outcome) + { + if (outcome == null) + { + return null; + } + + if (outcome.Value.Exception is not null) + { + return outcome.Value.Exception.Message; + } + + return _resultFormatter(context, outcome.Value.Result); + } +} diff --git a/src/Polly.Extensions/Telemetry/TelemetryOptions.cs b/src/Polly.Extensions/Telemetry/TelemetryOptions.cs index 67c65f4e055..7d0271934d9 100644 --- a/src/Polly.Extensions/Telemetry/TelemetryOptions.cs +++ b/src/Polly.Extensions/Telemetry/TelemetryOptions.cs @@ -11,12 +11,12 @@ namespace Polly.Telemetry; public class TelemetryOptions { /// - /// Gets or sets the callback that is raised when is received from Polly. + /// Gets or sets the optional user-specified telemetry listener. /// /// /// The default value is . /// - public Action? OnTelemetryEvent { get; set; } + public TelemetryListener? TelemetryListener { get; set; } /// /// Gets or sets the logger factory. @@ -33,7 +33,7 @@ public class TelemetryOptions /// /// The default value is an empty collection. /// - public ICollection> Enrichers { get; } = new List>(); + public ICollection MeteringEnrichers { get; } = new List(); /// /// Gets or sets the result formatter. diff --git a/src/Polly.Extensions/Telemetry/TelemetryResiliencePipelineBuilderExtensions.cs b/src/Polly.Extensions/Telemetry/TelemetryResiliencePipelineBuilderExtensions.cs index f65f396ea2e..bf738ff1c8e 100644 --- a/src/Polly.Extensions/Telemetry/TelemetryResiliencePipelineBuilderExtensions.cs +++ b/src/Polly.Extensions/Telemetry/TelemetryResiliencePipelineBuilderExtensions.cs @@ -51,7 +51,7 @@ public static TBuilder ConfigureTelemetry(this TBuilder builder, Telem Guard.NotNull(options); builder.Validator(new(options, $"The '{nameof(TelemetryOptions)}' are invalid.")); - builder.DiagnosticSource = new ResilienceTelemetryDiagnosticSource(options); + builder.TelemetryListener = new TelemetryListenerImpl(options); return builder; } diff --git a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs index fb17d6045e6..30ed127c236 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/CircuitBreakerResilienceStrategyTests.cs @@ -18,7 +18,7 @@ public CircuitBreakerResilienceStrategyTests() { _timeProvider = new FakeTimeProvider(); _behavior = Substitute.For(); - _telemetry = TestUtilities.CreateResilienceTelemetry(Substitute.For()); + _telemetry = TestUtilities.CreateResilienceTelemetry(Substitute.For()); _options = new CircuitBreakerStrategyOptions(); _controller = new CircuitStateController( CircuitBreakerConstants.DefaultBreakDuration, diff --git a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs index 41e0961b7ae..5323a7fecc9 100644 --- a/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs +++ b/test/Polly.Core.Tests/CircuitBreaker/Controller/CircuitStateControllerTests.cs @@ -11,7 +11,7 @@ public class CircuitStateControllerTests private readonly CircuitBreakerStrategyOptions _options = new(); private readonly CircuitBehavior _circuitBehavior = Substitute.For(); - private readonly Action _onTelemetry = _ => { }; + private readonly Action> _onTelemetry = _ => { }; [Fact] public void Ctor_EnsureDefaults() diff --git a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs index ecde38004c8..5e76eb77e41 100644 --- a/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Fallback/FallbackResilienceStrategyTests.cs @@ -7,7 +7,7 @@ namespace Polly.Core.Tests.Fallback; public class FallbackResilienceStrategyTests { private readonly FallbackStrategyOptions _options = new(); - private readonly List _args = new(); + private readonly List> _args = new(); private readonly ResilienceStrategyTelemetry _telemetry; private FallbackHandler? _handler; diff --git a/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs b/test/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs index 7ed4b7cdf2b..669bd4d18da 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.Duration, attempt.Handled)); + _args.Add(attempt); } }); diff --git a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs index f6e77b38a63..ba96439bae6 100644 --- a/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs @@ -16,7 +16,7 @@ public class HedgingResilienceStrategyTests : IDisposable private static readonly TimeSpan AssertTimeout = TimeSpan.FromSeconds(15); private readonly HedgingStrategyOptions _options = new(); - private readonly ConcurrentQueue _events = new(); + private readonly ConcurrentQueue> _events = new(); private readonly ResilienceStrategyTelemetry _telemetry; private readonly HedgingTimeProvider _timeProvider; private readonly HedgingActions _actions; diff --git a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs index 32be0e77487..a8305cc2863 100644 --- a/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs +++ b/test/Polly.Core.Tests/ResiliencePipelineBuilderTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; using Polly.Retry; +using Polly.Telemetry; using Polly.Testing; using Polly.Utils; @@ -25,13 +26,13 @@ public void CopyCtor_Ok() { TimeProvider = Substitute.For(), Name = "dummy", - DiagnosticSource = Substitute.For(), + TelemetryListener = Substitute.For(), }; var other = new ResiliencePipelineBuilder(builder); other.Name.Should().Be(builder.Name); other.TimeProvider.Should().Be(builder.TimeProvider); - other.DiagnosticSource.Should().BeSameAs(builder.DiagnosticSource); + other.TelemetryListener.Should().BeSameAs(builder.TelemetryListener); } [Fact] @@ -49,12 +50,12 @@ public void AddPipeline_Single_Ok() builder.AddPipeline(first.AsPipeline()); // act - var strategy = builder.Build(); + var pipeline = builder.Build(); // assert - strategy.Execute(_ => executions.Add(2)); + pipeline.Execute(_ => executions.Add(2)); - strategy.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); + pipeline.GetPipelineDescriptor().FirstStrategy.StrategyInstance.Should().BeOfType(); executions.Should().BeInAscendingOrder(); executions.Should().HaveCount(3); } diff --git a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs index f8c1bc51a6d..243222c9181 100644 --- a/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Retry/RetryResilienceStrategyTests.cs @@ -11,12 +11,12 @@ public class RetryResilienceStrategyTests { private readonly RetryStrategyOptions _options = new(); private readonly FakeTimeProvider _timeProvider = new(); - private readonly DiagnosticSource _diagnosticSource = Substitute.For(); + private readonly TelemetryListener _listener = Substitute.For(); private ResilienceStrategyTelemetry _telemetry; public RetryResilienceStrategyTests() { - _telemetry = TestUtilities.CreateResilienceTelemetry(_diagnosticSource); + _telemetry = TestUtilities.CreateResilienceTelemetry(_listener); _options.ShouldHandle = _ => new ValueTask(false); _options.Randomizer = () => 1; @@ -291,8 +291,6 @@ public async Task OnRetry_EnsureTelemetry() var attempts = new List(); var delays = new List(); - _diagnosticSource.IsEnabled("OnRetry").Returns(true); - _options.ShouldHandle = args => args.Outcome.ResultPredicateAsync(0); _options.RetryCount = 3; _options.BackoffType = RetryBackoffType.Linear; @@ -301,7 +299,7 @@ public async Task OnRetry_EnsureTelemetry() await ExecuteAndAdvance(sut); - _diagnosticSource.Received(3).IsEnabled("OnRetry"); + _listener.ReceivedWithAnyArgs(3).Write(default); } [Fact] diff --git a/test/Polly.Core.Tests/StrategyBuilderContextTests.cs b/test/Polly.Core.Tests/StrategyBuilderContextTests.cs index d14f9a7e360..b493164744f 100644 --- a/test/Polly.Core.Tests/StrategyBuilderContextTests.cs +++ b/test/Polly.Core.Tests/StrategyBuilderContextTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Time.Testing; using NSubstitute; +using Polly.Telemetry; namespace Polly.Core.Tests; @@ -9,7 +10,7 @@ public class StrategyBuilderContextTests public void Ctor_EnsureDefaults() { var timeProvider = new FakeTimeProvider(); - var context = new StrategyBuilderContext("builder-name", "instance", "strategy-name", timeProvider, Substitute.For()); + var context = new StrategyBuilderContext("builder-name", "instance", "strategy-name", timeProvider, Substitute.For()); context.BuilderName.Should().Be("builder-name"); context.BuilderInstanceName.Should().Be("instance"); diff --git a/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs b/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs index 1042b4574aa..aac43591052 100644 --- a/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs +++ b/test/Polly.Core.Tests/Telemetry/ExecutionAttemptArgumentsTests.cs @@ -8,31 +8,8 @@ public class ExecutionAttemptArgumentsTests public void Ctor_Ok() { var args = new ExecutionAttemptArguments(99, TimeSpan.MaxValue, true); - Assert.NotNull(args); args.AttemptNumber.Should().Be(99); args.Duration.Should().Be(TimeSpan.MaxValue); args.Handled.Should().BeTrue(); } - - [Fact] - public void Get_Ok() - { - var args = ExecutionAttemptArguments.Get(99, TimeSpan.MaxValue, true); - Assert.NotNull(args); - args.AttemptNumber.Should().Be(99); - args.Duration.Should().Be(TimeSpan.MaxValue); - args.Handled.Should().BeTrue(); - } - - [Fact] - public void Return_EnsurePropertiesCleared() - { - var args = ExecutionAttemptArguments.Get(99, TimeSpan.MaxValue, true); - - ExecutionAttemptArguments.Return(args); - - args.AttemptNumber.Should().Be(0); - 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 index 50a04240285..2c436449d44 100644 --- a/test/Polly.Core.Tests/Telemetry/PipelineExecutedArgumentsTests.cs +++ b/test/Polly.Core.Tests/Telemetry/PipelineExecutedArgumentsTests.cs @@ -10,22 +10,4 @@ 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/Telemetry/ResilienceStrategyTelemetryTests.cs b/test/Polly.Core.Tests/Telemetry/ResilienceStrategyTelemetryTests.cs index 91b9fcf47f2..29ebb802d4a 100644 --- a/test/Polly.Core.Tests/Telemetry/ResilienceStrategyTelemetryTests.cs +++ b/test/Polly.Core.Tests/Telemetry/ResilienceStrategyTelemetryTests.cs @@ -1,52 +1,37 @@ -using NSubstitute; using Polly.Telemetry; namespace Polly.Core.Tests.Telemetry; public class ResilienceStrategyTelemetryTests { - private readonly DiagnosticSource _diagnosticSource = Substitute.For(); - - public ResilienceStrategyTelemetryTests() => _sut = new(new ResilienceTelemetrySource( - "builder", - "instance", - "strategy-name"), _diagnosticSource); - + private readonly List> _args = new(); + private readonly ResilienceTelemetrySource _source; private readonly ResilienceStrategyTelemetry _sut; - [Fact] - public void Report_NoOutcome_OK() + public ResilienceStrategyTelemetryTests() { - _diagnosticSource.IsEnabled("dummy-event").Returns(true); - _diagnosticSource - .When(o => o.Write("dummy-event", Arg.Is(obj => obj is TelemetryEventArguments))) - .Do((p) => - { - p[1].Should().BeOfType(); - var args = (TelemetryEventArguments)p[1]; - - args.Event.EventName.Should().Be("dummy-event"); - args.Event.Severity.Should().Be(ResilienceEventSeverity.Warning); - args.Outcome.Should().BeNull(); - args.Source.StrategyName.Should().Be("strategy-name"); - args.Arguments.Should().BeOfType(); - args.Outcome.Should().BeNull(); - args.Context.Should().NotBeNull(); - }); - - _sut.Report(new(ResilienceEventSeverity.Warning, "dummy-event"), ResilienceContextPool.Shared.Get(), new TestArguments()); + _source = new ResilienceTelemetrySource( + "builder", + "instance", + "strategy-name"); - _diagnosticSource.Received().Write("dummy-event", Arg.Is(obj => obj is TelemetryEventArguments)); + _sut = TestUtilities.CreateResilienceTelemetry(args => _args.Add(args)); } [Fact] - public void Report_NoOutcomeWhenNotSubscribed_None() + public void Report_NoOutcome_OK() { - _diagnosticSource.IsEnabled("dummy-event").Returns(false); - _sut.Report(new(ResilienceEventSeverity.Warning, "dummy-event"), ResilienceContextPool.Shared.Get(), new TestArguments()); - _diagnosticSource.DidNotReceiveWithAnyArgs().Write(default!, default); + _args.Should().HaveCount(1); + var args = _args.Single(); + args.Event.EventName.Should().Be("dummy-event"); + args.Event.Severity.Should().Be(ResilienceEventSeverity.Warning); + args.Outcome.Should().BeNull(); + args.Source.StrategyName.Should().Be("strategy-name"); + args.Arguments.Should().BeOfType(); + args.Outcome.Should().BeNull(); + args.Context.Should().NotBeNull(); } [Fact] @@ -63,48 +48,43 @@ public void ResiliencePipelineTelemetry_NoDiagnosticSource_Ok() [Fact] public void Report_Outcome_OK() { - _diagnosticSource.IsEnabled("dummy-event").Returns(true); - _diagnosticSource - .When(o => o.Write("dummy-event", Arg.Is(obj => obj is TelemetryEventArguments))) - .Do((obj) => - { - obj[1].Should().BeOfType(); - var args = (TelemetryEventArguments)obj[1]; - - args.Event.EventName.Should().Be("dummy-event"); - args.Event.Severity.Should().Be(ResilienceEventSeverity.Warning); - args.Source.StrategyName.Should().Be("strategy-name"); - args.Arguments.Should().BeOfType(); - args.Outcome.Should().NotBeNull(); - args.Outcome!.Value.Result.Should().Be(99); - args.Context.Should().NotBeNull(); - }); - var context = ResilienceContextPool.Shared.Get(); _sut.Report(new(ResilienceEventSeverity.Warning, "dummy-event"), new OutcomeArguments(context, Outcome.FromResult(99), new TestArguments())); - _diagnosticSource.Received().Write("dummy-event", Arg.Is(obj => obj is TelemetryEventArguments)); + _args.Should().HaveCount(1); + var args = _args.Single(); + args.Event.EventName.Should().Be("dummy-event"); + args.Event.Severity.Should().Be(ResilienceEventSeverity.Warning); + args.Source.StrategyName.Should().Be("strategy-name"); + args.Arguments.Should().BeOfType(); + args.Outcome.Should().NotBeNull(); + args.Outcome!.Value.Result.Should().Be(99); + args.Context.Should().NotBeNull(); } [Fact] public void Report_SeverityNone_Skipped() { - _diagnosticSource.IsEnabled("dummy-event").Returns(true); - var context = ResilienceContextPool.Shared.Get(); _sut.Report(new(ResilienceEventSeverity.None, "dummy-event"), new OutcomeArguments(context, Outcome.FromResult(99), new TestArguments())); _sut.Report(new(ResilienceEventSeverity.None, "dummy-event"), ResilienceContextPool.Shared.Get(), new TestArguments()); - _diagnosticSource.DidNotReceiveWithAnyArgs().Write(default!, default); + _args.Should().BeEmpty(); } [Fact] - public void Report_OutcomeWhenNotSubscribed_None() + public void Report_NoListener_ShouldNotThrow() { - _diagnosticSource.IsEnabled("dummy-event").Returns(false); + var sut = new ResilienceStrategyTelemetry(_source, null); + var context = ResilienceContextPool.Shared.Get(); - _sut.Report(new(ResilienceEventSeverity.Warning, "dummy-event"), new OutcomeArguments(context, Outcome.FromResult(10), new TestArguments())); - _diagnosticSource.DidNotReceiveWithAnyArgs().Write(default!, default); + sut.Invoking(s => s.Report(new(ResilienceEventSeverity.None, "dummy-event"), new OutcomeArguments(context, Outcome.FromResult(99), new TestArguments()))) + .Should() + .NotThrow(); + + sut.Invoking(s => s.Report(new(ResilienceEventSeverity.None, "dummy-event"), ResilienceContextPool.Shared.Get(), new TestArguments())) + .Should() + .NotThrow(); } } diff --git a/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs b/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs index b0701748942..ec6a68bf1b9 100644 --- a/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs +++ b/test/Polly.Core.Tests/Telemetry/TelemetryEventArgumentsTests.cs @@ -7,10 +7,10 @@ public class TelemetryEventArgumentsTests private readonly ResilienceTelemetrySource _source = new("builder", "instance", "strategy"); [Fact] - public void Get_Ok() + public void Ctor_Ok() { var context = ResilienceContextPool.Shared.Get(); - var args = TelemetryEventArguments.Get(_source, new ResilienceEvent(ResilienceEventSeverity.Warning, "ev"), context, Outcome.FromResult("dummy"), "arg"); + var args = new TelemetryEventArguments(_source, new ResilienceEvent(ResilienceEventSeverity.Warning, "ev"), context, "arg", Outcome.FromResult("dummy")); args.Outcome!.Value.Result.Should().Be("dummy"); args.Context.Should().Be(context); @@ -20,23 +20,4 @@ public void Get_Ok() args.Arguments.Should().BeEquivalentTo("arg"); args.Context.Should().Be(context); } - - [Fact] - public void Return_EnsurePropertiesCleared() - { - var context = ResilienceContextPool.Shared.Get(); - var args = TelemetryEventArguments.Get(_source, new ResilienceEvent(ResilienceEventSeverity.Warning, "ev"), context, Outcome.FromResult("dummy"), "arg"); - - TelemetryEventArguments.Return(args); - - TestUtilities.AssertWithTimeoutAsync(() => - { - args.Outcome.Should().BeNull(); - args.Context.Should().BeNull(); - args.Event.EventName.Should().BeNullOrEmpty(); - args.Source.Should().BeNull(); - args.Arguments.Should().BeNull(); - args.Context.Should().BeNull(); - }); - } } diff --git a/test/Polly.Core.Tests/Telemetry/TelemetryUtilTests.cs b/test/Polly.Core.Tests/Telemetry/TelemetryUtilTests.cs index 6e7936aa656..ea8a6ee5ca7 100644 --- a/test/Polly.Core.Tests/Telemetry/TelemetryUtilTests.cs +++ b/test/Polly.Core.Tests/Telemetry/TelemetryUtilTests.cs @@ -12,7 +12,7 @@ public void CreateResilienceTelemetry_Ok() telemetry.TelemetrySource.PipelineName.Should().Be("builder"); telemetry.TelemetrySource.PipelineInstanceName.Should().Be("instance"); telemetry.TelemetrySource.StrategyName.Should().Be("strategy-name"); - telemetry.DiagnosticSource.Should().BeNull(); + telemetry.Listener.Should().BeNull(); } [InlineData(true, ResilienceEventSeverity.Warning)] @@ -22,13 +22,13 @@ public void ReportExecutionAttempt_Ok(bool handled, ResilienceEventSeverity seve { var asserted = false; var props = new ResilienceProperties(); - var telemetry = TestUtilities.CreateResilienceTelemetry(args => + var listener = TestUtilities.CreateResilienceTelemetry(args => { args.Event.Severity.Should().Be(severity); asserted = true; }); - TelemetryUtil.ReportExecutionAttempt(telemetry, ResilienceContextPool.Shared.Get(), Outcome.FromResult("dummy"), 0, TimeSpan.Zero, handled); + TelemetryUtil.ReportExecutionAttempt(listener, ResilienceContextPool.Shared.Get(), Outcome.FromResult("dummy"), 0, TimeSpan.Zero, handled); asserted.Should().BeTrue(); } } diff --git a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs index 2909c0311e7..c165b719457 100644 --- a/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs +++ b/test/Polly.Core.Tests/Timeout/TimeoutResilienceStrategyTests.cs @@ -12,12 +12,11 @@ public class TimeoutResilienceStrategyTests : IDisposable private readonly TimeoutStrategyOptions _options; private readonly CancellationTokenSource _cancellationSource; private readonly TimeSpan _delay = TimeSpan.FromSeconds(12); - - private readonly DiagnosticSource _diagnosticSource = Substitute.For(); + private readonly List> _args = new(); public TimeoutResilienceStrategyTests() { - _telemetry = TestUtilities.CreateResilienceTelemetry(_diagnosticSource); + _telemetry = TestUtilities.CreateResilienceTelemetry(arg => _args.Add(arg)); _options = new TimeoutStrategyOptions(); _cancellationSource = new CancellationTokenSource(); } @@ -52,8 +51,6 @@ public void Execute_EnsureTimeoutGeneratorCalled() [Fact] public async Task Execute_EnsureOnTimeoutCalled() { - _diagnosticSource.IsEnabled("OnTimeout").Returns(true); - var called = false; SetTimeout(_delay); @@ -77,7 +74,9 @@ await sut.Invoking(s => sut.ExecuteAsync(async token => .AsTask()).Should().ThrowAsync(); called.Should().BeTrue(); - _diagnosticSource.Received().IsEnabled("OnTimeout"); + + _args.Should().HaveCount(1); + _args[0].Arguments.Should().BeOfType(); } [MemberData(nameof(Execute_NoTimeout_Data))] @@ -163,7 +162,7 @@ await sut.Invoking(s => s.ExecuteAsync(async token => onTimeoutCalled.Should().BeFalse(); - _diagnosticSource.DidNotReceive().IsEnabled("OnTimeout"); + _args.Should().HaveCount(0); } [Fact] diff --git a/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs b/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs index d1c37cddd09..3a1cf7ee701 100644 --- a/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs +++ b/test/Polly.Core.Tests/Utils/CompositeResiliencePipelineTests.cs @@ -8,7 +8,7 @@ namespace Polly.Core.Tests.Utils; public class CompositeResiliencePipelineTests { private readonly ResilienceStrategyTelemetry _telemetry; - private Action? _onTelemetry; + private Action>? _onTelemetry; public CompositeResiliencePipelineTests() => _telemetry = TestUtilities.CreateResilienceTelemetry(args => _onTelemetry?.Invoke(args)); @@ -138,7 +138,7 @@ public void ExecuptePipeline_EnsureTelemetryArgumentsReported() pipeline.Execute(() => { timeProvider.Advance(TimeSpan.FromHours(1)); }); items.Should().HaveCount(2); - items[0].Should().Be(PipelineExecutingArguments.Instance); + items[0].Should().BeOfType(); items[1].Should().BeOfType(); } diff --git a/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs b/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs index 8baacab9610..4870007c7f0 100644 --- a/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs +++ b/test/Polly.Core.Tests/Utils/ReloadableResiliencePipelineTests.cs @@ -5,7 +5,7 @@ namespace Polly.Core.Tests.Utils; public class ReloadableResiliencePipelineTests : IDisposable { - private readonly List _events = new(); + private readonly List> _events = new(); private readonly ResilienceStrategyTelemetry _telemetry; private CancellationTokenSource _cancellationTokenSource; diff --git a/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs b/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs index 7e7967d66ae..ba2b3f20b21 100644 --- a/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs +++ b/test/Polly.Extensions.Tests/DependencyInjection/PollyServiceCollectionExtensionTests.cs @@ -144,8 +144,8 @@ public void AddResiliencePipeline_EnsureTelemetryEnabled(bool hasLogging) CreateProvider().GetPipeline(Key); - var diagSource = telemetry!.GetType().GetProperty("DiagnosticSource", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(telemetry); - diagSource.Should().BeOfType(); + var diagSource = telemetry!.GetType().GetProperty("Listener", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(telemetry); + diagSource.Should().BeOfType(); var factory = _services.BuildServiceProvider().GetRequiredService>().Value.LoggerFactory; @@ -256,7 +256,7 @@ public void AddResiliencePipelineRegistry_Ok() provider.GetRequiredService>().Should().NotBeNull(); provider.GetRequiredService>().Should().NotBeNull(); - provider.GetRequiredService().DiagnosticSource.Should().NotBeNull(); + provider.GetRequiredService().TelemetryListener.Should().NotBeNull(); } [Fact] diff --git a/test/Polly.Extensions.Tests/Telemetry/EnrichmentContextTests.cs b/test/Polly.Extensions.Tests/Telemetry/EnrichmentContextTests.cs deleted file mode 100644 index fbe6e67be2f..00000000000 --- a/test/Polly.Extensions.Tests/Telemetry/EnrichmentContextTests.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Polly.Telemetry; - -namespace Polly.Extensions.Tests.Telemetry; - -public class EnrichmentContextTests -{ - [Fact] - public async Task Pooling_OK() - { - await TestUtilities.AssertWithTimeoutAsync(() => - { - var context = EnrichmentContext.Get(ResilienceContextPool.Shared.Get(), null, null); - - EnrichmentContext.Return(context); - - EnrichmentContext.Get(ResilienceContextPool.Shared.Get(), null, null).Should().BeSameAs(context); - }); - } -} diff --git a/test/Polly.Extensions.Tests/Telemetry/TagsListTests.cs b/test/Polly.Extensions.Tests/Telemetry/TagsListTests.cs new file mode 100644 index 00000000000..aa4fa0b3c64 --- /dev/null +++ b/test/Polly.Extensions.Tests/Telemetry/TagsListTests.cs @@ -0,0 +1,20 @@ +using Polly.Telemetry; + +namespace Polly.Extensions.Tests.Telemetry; + +[Collection(nameof(NonParallelizableCollection))] +public class TagsListTests +{ + [Fact] + public async Task Pooling_OK() + { + await TestUtilities.AssertWithTimeoutAsync(() => + { + var context = TagsList.Get(); + + TagsList.Return(context); + + TagsList.Get().Should().BeSameAs(context); + }); + } +} diff --git a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs b/test/Polly.Extensions.Tests/Telemetry/TelemetryListenerImplTests.cs similarity index 87% rename from test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs rename to test/Polly.Extensions.Tests/Telemetry/TelemetryListenerImplTests.cs index 0ae7e91fe86..1c3db8740e2 100644 --- a/test/Polly.Extensions.Tests/Telemetry/ResilienceTelemetryDiagnosticSourceTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/TelemetryListenerImplTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net.Http; using Microsoft.Extensions.Logging; using Polly.Telemetry; @@ -7,15 +8,15 @@ namespace Polly.Extensions.Tests.Telemetry; #pragma warning disable S103 // Lines should not be too long [Collection(nameof(NonParallelizableCollection))] -public class ResilienceTelemetryDiagnosticSourceTests : IDisposable +public class TelemetryListenerImplTests : IDisposable { private readonly FakeLogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly List _events = new(); - private Action? _onEvent; + private Action>? _onEvent; private IDisposable _metering; - public ResilienceTelemetryDiagnosticSourceTests() + public TelemetryListenerImplTests() { _metering = TestUtilities.EnablePollyMetering(_events); _loggerFactory = TestUtilities.CreateLoggerFactory(out _logger); @@ -30,9 +31,9 @@ public void Dispose() [Fact] public void Meter_Ok() { - ResilienceTelemetryDiagnosticSource.Meter.Name.Should().Be("Polly"); - ResilienceTelemetryDiagnosticSource.Meter.Version.Should().Be("1.0"); - var source = new ResilienceTelemetryDiagnosticSource(new TelemetryOptions()); + TelemetryListenerImpl.Meter.Name.Should().Be("Polly"); + TelemetryListenerImpl.Meter.Version.Should().Be("1.0"); + var source = new TelemetryListenerImpl(new TelemetryOptions()); 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."); @@ -42,24 +43,6 @@ public void Meter_Ok() source.ExecutionDuration.Unit.Should().Be("ms"); } - [Fact] - public void IsEnabled_true() - { - var source = Create(); - - source.IsEnabled("dummy").Should().BeTrue(); - } - - [Fact] - public void Write_InvalidType_Nothing() - { - var source = Create(); - - source.Write("dummy", new object()); - - _logger.GetRecords().Should().BeEmpty(); - } - [InlineData(true)] [InlineData(false)] [Theory] @@ -313,20 +296,21 @@ public void WriteExecutionAttemptEvent_ShouldBeSkipped() public void WriteEvent_MeteringWithEnrichers_Ok(int count) { const int DefaultDimensions = 7; - var telemetry = Create(enrichers => + + var telemetry = Create(new[] { - enrichers.Add(context => + new CallbackEnricher(context => { for (int i = 0; i < count; i++) { context.Tags.Add(new KeyValuePair($"custom-{i}", $"custom-{i}-value")); } - }); + }), - enrichers.Add(context => + new CallbackEnricher(context => { context.Tags.Add(new KeyValuePair("other", "other-value")); - }); + }) }); ReportEvent(telemetry, Outcome.FromResult(true)); @@ -386,7 +370,7 @@ public void PipelineExecution_Logged(bool healthy, bool exception) ((List)context.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); } - ReportEvent(telemetry, outcome: outcome, arg: new PipelineExecutingArguments(), context: context); + ReportEvent(telemetry, outcome: outcome, arg: default(PipelineExecutingArguments), context: context); ReportEvent(telemetry, outcome: outcome, arg: new PipelineExecutedArguments(TimeSpan.FromSeconds(10)), context: context); var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); @@ -403,7 +387,7 @@ 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); + ReportEvent(telemetry, outcome: null, arg: default(PipelineExecutingArguments), context: context); var messages = _logger.GetRecords(new EventId(1, "StrategyExecuting")).ToList(); messages.Should().HaveCount(1); @@ -439,17 +423,17 @@ public void PipelineExecution_Metered(bool healthy, bool exception) ((List)context.ResilienceEvents).Add(new ResilienceEvent(ResilienceEventSeverity.Warning, "dummy")); } - var telemetry = Create(enrichers => + var telemetry = Create(new[] { - enrichers.Add(context => + new CallbackEnricher(context => { if (exception) { - context.Outcome!.Value.Exception.Should().BeOfType(); + context.TelemetryEvent.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); @@ -501,35 +485,63 @@ public void PipelineExecuted_ShouldBeSkipped() private List> GetEvents(string eventName) => _events.Where(e => e.Name == eventName).Select(v => v.Tags).ToList(); - private ResilienceTelemetryDiagnosticSource Create(Action>>? configureEnrichers = null) + private TelemetryListenerImpl Create(IEnumerable? enrichers = null) { var options = new TelemetryOptions { LoggerFactory = _loggerFactory, - OnTelemetryEvent = _onEvent }; - configureEnrichers?.Invoke(options.Enrichers); + if (_onEvent is not null) + { + options.TelemetryListener = new CallbackTelemetryListener(_onEvent); + } + + if (enrichers != null) + { + foreach (var enricher in enrichers) + { + options.MeteringEnrichers.Add(enricher); + } + } return new(options); } private static void ReportEvent( - ResilienceTelemetryDiagnosticSource telemetry, + TelemetryListenerImpl telemetry, + Outcome? outcome, + string? instanceName = "pipeline-instance", + ResilienceContext? context = null, + TestArguments? arg = null, + ResilienceEventSeverity severity = ResilienceEventSeverity.Warning) => ReportEvent(telemetry, outcome, instanceName, context, arg!, severity); + + private static void ReportEvent( + TelemetryListenerImpl telemetry, Outcome? outcome, string? instanceName = "pipeline-instance", ResilienceContext? context = null, - object? arg = null, + TArgs arg = default!, ResilienceEventSeverity severity = ResilienceEventSeverity.Warning) { - context ??= ResilienceContextPool.Shared.Get("op-key"); - telemetry.ReportEvent( - new ResilienceEvent(severity, "my-event"), - "my-pipeline", - instanceName, - "my-strategy", - context, - outcome, - arg ?? new TestArguments()); + telemetry.Write( + new TelemetryEventArguments( + new ResilienceTelemetrySource("my-pipeline", instanceName, "my-strategy"), + new ResilienceEvent(severity, "my-event"), + context ?? ResilienceContextPool.Shared.Get("op-key"), + arg!, + outcome)); + } + + private class CallbackEnricher : MeteringEnricher + { + private readonly Action> _callback; + + public CallbackEnricher(Action> callback) => _callback = callback; + + public override void Enrich(in EnrichmentContext context) + { + _callback(new EnrichmentContext(context.TelemetryEvent.AsObjectArguments(), context.Tags)); + } } } diff --git a/test/Polly.Extensions.Tests/Telemetry/TelemetryOptionsTests.cs b/test/Polly.Extensions.Tests/Telemetry/TelemetryOptionsTests.cs index e2fdd2f95bb..4f0e8ae0a23 100644 --- a/test/Polly.Extensions.Tests/Telemetry/TelemetryOptionsTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/TelemetryOptionsTests.cs @@ -12,7 +12,7 @@ public void Ctor_EnsureDefaults() { var options = new TelemetryOptions(); - options.Enrichers.Should().BeEmpty(); + options.MeteringEnrichers.Should().BeEmpty(); options.LoggerFactory.Should().Be(NullLoggerFactory.Instance); var resilienceContext = ResilienceContextPool.Shared.Get(); options.ResultFormatter(resilienceContext, null).Should().BeNull(); diff --git a/test/Polly.Extensions.Tests/Telemetry/TelemetryResiliencePipelineBuilderExtensionsTests.cs b/test/Polly.Extensions.Tests/Telemetry/TelemetryResiliencePipelineBuilderExtensionsTests.cs index 2864affbf67..bea1ca46227 100644 --- a/test/Polly.Extensions.Tests/Telemetry/TelemetryResiliencePipelineBuilderExtensionsTests.cs +++ b/test/Polly.Extensions.Tests/Telemetry/TelemetryResiliencePipelineBuilderExtensionsTests.cs @@ -12,7 +12,7 @@ public class TelemetryResiliencePipelineBuilderExtensionsTests public void ConfigureTelemetry_EnsureDiagnosticSourceUpdated() { _builder.ConfigureTelemetry(NullLoggerFactory.Instance); - _builder.DiagnosticSource.Should().BeOfType(); + _builder.TelemetryListener.Should().BeOfType(); _builder.AddStrategy(new TestResilienceStrategy()).Build().Should().NotBeOfType(); } diff --git a/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs b/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs index df1e066d741..6263df09b48 100644 --- a/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs +++ b/test/Polly.RateLimiting.Tests/RateLimiterResilienceStrategyTests.cs @@ -1,5 +1,6 @@ using System.Threading.RateLimiting; using NSubstitute; +using Polly.Telemetry; namespace Polly.RateLimiting.Tests; @@ -7,7 +8,7 @@ public class RateLimiterResilienceStrategyTests { private readonly RateLimiter _limiter = Substitute.For(); private readonly RateLimitLease _lease = Substitute.For(); - private readonly DiagnosticSource _diagnosticSource = Substitute.For(); + private readonly TelemetryListener _listener = Substitute.For(); private Func? _event; [Fact] @@ -42,9 +43,6 @@ public async Task Execute_HappyPath() [Theory] public async Task Execute_LeaseRejected(bool hasEvents, bool hasRetryAfter) { - _diagnosticSource.IsEnabled("OnRateLimiterRejected").Returns(true); - _diagnosticSource.Write("OnRateLimiterRejected", Arg.Is(obj => obj != null)); - object? metadata = hasRetryAfter ? TimeSpan.FromSeconds(123) : null; using var cts = new CancellationTokenSource(); @@ -87,6 +85,8 @@ public async Task Execute_LeaseRejected(bool hasEvents, bool hasRetryAfter) await _limiter.ReceivedWithAnyArgs().AcquireAsync(default, default); _lease.Received().Dispose(); + + _listener.ReceivedWithAnyArgs(1).Write(default); } private void SetupLimiter(CancellationToken token) @@ -103,7 +103,7 @@ private ResiliencePipeline Create() { var builder = new ResiliencePipelineBuilder { - DiagnosticSource = _diagnosticSource + TelemetryListener = _listener }; return builder.AddRateLimiter(new RateLimiterStrategyOptions diff --git a/test/Polly.TestUtils/CallbackTelemetryListener.cs b/test/Polly.TestUtils/CallbackTelemetryListener.cs new file mode 100644 index 00000000000..73045a953d4 --- /dev/null +++ b/test/Polly.TestUtils/CallbackTelemetryListener.cs @@ -0,0 +1,19 @@ +using Polly.Telemetry; + +namespace Polly.TestUtils; + +public sealed class CallbackTelemetryListener : TelemetryListener +{ + private readonly Action> _callback; + private readonly object _syncRoot = new(); + + public CallbackTelemetryListener(Action> callback) => _callback = callback; + + public override void Write(in TelemetryEventArguments args) + { + lock (_syncRoot) + { + _callback(args.AsObjectArguments()); + } + } +} diff --git a/test/Polly.TestUtils/TestUtilities.cs b/test/Polly.TestUtils/TestUtilities.cs index 484ff494db6..91263a3ed3d 100644 --- a/test/Polly.TestUtils/TestUtilities.cs +++ b/test/Polly.TestUtils/TestUtilities.cs @@ -37,11 +37,11 @@ public static async Task AssertWithTimeoutAsync(Func assertion, TimeSpan t } } - public static ResilienceStrategyTelemetry CreateResilienceTelemetry(DiagnosticSource source) - => new(new ResilienceTelemetrySource("dummy-builder", "dummy-instance", "strategy-name"), source); + public static ResilienceStrategyTelemetry CreateResilienceTelemetry(TelemetryListener listener) + => new(new ResilienceTelemetrySource("dummy-builder", "dummy-instance", "strategy-name"), listener); - public static ResilienceStrategyTelemetry CreateResilienceTelemetry(Action callback) - => new(new ResilienceTelemetrySource("dummy-builder", "dummy-instance", "strategy-name"), new CallbackDiagnosticSource(callback)); + public static ResilienceStrategyTelemetry CreateResilienceTelemetry(Action> callback, ResilienceTelemetrySource? source = null) + => new(source ?? new ResilienceTelemetrySource("dummy-builder", "dummy-instance", "strategy-name"), new CallbackTelemetryListener(callback)); public static ILoggerFactory CreateLoggerFactory(out FakeLogger logger) { @@ -85,26 +85,6 @@ void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan return meterListener; } -#pragma warning disable S107 // Methods should not have too many parameters - public static void ReportEvent( - this DiagnosticSource source, - ResilienceEvent resilienceEvent, - string builderName, - string? instanceName, - string? strategyName, - ResilienceContext context, - Outcome? outcome, - object arguments) -#pragma warning restore S107 // Methods should not have too many parameters - { - source.Write(resilienceEvent.EventName, TelemetryEventArguments.Get( - new ResilienceTelemetrySource(builderName, instanceName, strategyName), - resilienceEvent, - context, - outcome, - arguments)); - } - public static ResilienceContext WithResultType(this ResilienceContext context) { context.Initialize(true); @@ -117,32 +97,13 @@ public static ResilienceContext WithVoidResultType(this ResilienceContext contex return context; } - private sealed class CallbackDiagnosticSource : DiagnosticSource + public static TelemetryEventArguments AsObjectArguments(this TelemetryEventArguments args) { - private readonly Action _callback; - private readonly object _syncRoot = new(); - - public CallbackDiagnosticSource(Action callback) => _callback = callback; - - public override bool IsEnabled(string name) => true; - - public override void Write(string name, object? value) - { - var args = (TelemetryEventArguments)value!; - var arguments = args.Arguments; - - if (arguments is ExecutionAttemptArguments attempt) - { - arguments = ExecutionAttemptArguments.Get(attempt.AttemptNumber, attempt.Duration, attempt.Handled); - } - - // copy the args because these are pooled and in tests we want to preserve them - args = TelemetryEventArguments.Get(args.Source, args.Event, args.Context, args.Outcome, arguments); - - lock (_syncRoot) - { - _callback(args); - } - } + return new TelemetryEventArguments( + args.Source, + args.Event, + args.Context, + args.Arguments!, + args.Outcome.HasValue ? args.Outcome.Value.AsOutcome() : null); } }