From 4c1cd6e4c2ddb06b9e1c2b149303f7ffaf4c52e0 Mon Sep 17 00:00:00 2001 From: martintmk <103487740+martintmk@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:37:40 +0100 Subject: [PATCH] Introduce ResilienceTelemetryFactory (#1073) --- .../ResilienceStrategyBuilderContextTests.cs | 21 +++++++++ .../Builder/ResilienceStrategyBuilderTests.cs | 45 +++++++++++++++++++ .../NullResilienceTelemetryFactoryTests.cs | 25 +++++++++++ .../Telemetry/NullResilienceTelemetryTests.cs | 29 ++++++++++++ .../Builder/ResilienceStrategyBuilder.cs | 23 +++++++--- .../ResilienceStrategyBuilderContext.cs | 27 +++++------ .../ResilienceStrategyBuilderOptions.cs | 7 +++ .../Telemetry/NullResilienceTelemetry.cs | 31 +++++++++++++ .../NullResilienceTelemetryFactory.cs | 19 ++++++++ .../Telemetry/ResilienceTelemetry.cs | 36 +++++++++++++++ .../Telemetry/ResilienceTelemetryFactory.cs | 16 +++++++ .../ResilienceTelemetryFactoryContext.cs | 27 +++++++++++ 12 files changed, 285 insertions(+), 21 deletions(-) create mode 100644 src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderContextTests.cs create mode 100644 src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryFactoryTests.cs create mode 100644 src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryTests.cs create mode 100644 src/Polly.Core/Telemetry/NullResilienceTelemetry.cs create mode 100644 src/Polly.Core/Telemetry/NullResilienceTelemetryFactory.cs create mode 100644 src/Polly.Core/Telemetry/ResilienceTelemetry.cs create mode 100644 src/Polly.Core/Telemetry/ResilienceTelemetryFactory.cs create mode 100644 src/Polly.Core/Telemetry/ResilienceTelemetryFactoryContext.cs diff --git a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderContextTests.cs b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderContextTests.cs new file mode 100644 index 00000000000..9e89eb903d1 --- /dev/null +++ b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderContextTests.cs @@ -0,0 +1,21 @@ +using FluentAssertions; +using Polly.Builder; +using Polly.Telemetry; +using Xunit; + +namespace Polly.Core.Tests.Builder; + +public class ResilienceStrategyBuilderContextTests +{ + [Fact] + public void Ctor_EnsureDefaults() + { + var context = new ResilienceStrategyBuilderContext(); + + context.BuilderName.Should().Be(""); + context.BuilderProperties.Should().NotBeNull(); + context.StrategyName.Should().Be(""); + context.StrategyType.Should().Be(""); + context.Telemetry.Should().Be(NullResilienceTelemetry.Instance); + } +} \ No newline at end of file diff --git a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs index 68cd3cd4df9..dc1c84cb160 100644 --- a/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs +++ b/src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs @@ -1,8 +1,10 @@ using System; using System.ComponentModel.DataAnnotations; using FluentAssertions; +using Moq; using Polly.Builder; using Polly.Core.Tests.Utils; +using Polly.Telemetry; using Xunit; namespace Polly.Core.Tests.Builder; @@ -261,6 +263,8 @@ public void BuildStrategy_EnsureCorrectContext() context.StrategyName.Should().Be("strategy-name"); context.StrategyType.Should().Be("strategy-type"); context.BuilderProperties.Should().BeSameAs(builder.Options.Properties); + context.Telemetry.Should().NotBeNull(); + context.Telemetry.Should().Be(NullResilienceTelemetry.Instance); verified1 = true; return new TestResilienceStrategy(); @@ -274,6 +278,8 @@ public void BuildStrategy_EnsureCorrectContext() context.StrategyName.Should().Be("strategy-name-2"); context.StrategyType.Should().Be("strategy-type-2"); context.BuilderProperties.Should().BeSameAs(builder.Options.Properties); + context.Telemetry.Should().NotBeNull(); + context.Telemetry.Should().Be(NullResilienceTelemetry.Instance); verified2 = true; return new TestResilienceStrategy(); @@ -288,6 +294,45 @@ public void BuildStrategy_EnsureCorrectContext() verified2.Should().BeTrue(); } + [Fact] + public void BuildStrategy_EnsureTelemetryFactoryInvoked() + { + // arrange + var factory = new Mock(MockBehavior.Strict); + var builder = new ResilienceStrategyBuilder + { + Options = new ResilienceStrategyBuilderOptions + { + BuilderName = "builder-name", + TelemetryFactory = factory.Object + }, + }; + + factory + .Setup(v => v.Create(It.IsAny())) + .Returns(NullResilienceTelemetry.Instance) + .Callback(context => + { + context.BuilderName.Should().Be("builder-name"); + context.StrategyName.Should().Be("strategy-name"); + context.StrategyType.Should().Be("strategy-type"); + context.BuilderProperties.Should().BeSameAs(builder.Options.Properties); + }); + + builder.AddStrategy( + context => + { + return new TestResilienceStrategy(); + }, + new ResilienceStrategyOptions { StrategyName = "strategy-name", StrategyType = "strategy-type" }); + + // act + builder.Build(); + + // assert + factory.VerifyAll(); + } + private class Strategy : ResilienceStrategy { public Action? Before { get; set; } diff --git a/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryFactoryTests.cs b/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryFactoryTests.cs new file mode 100644 index 00000000000..c4aef300ea3 --- /dev/null +++ b/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryFactoryTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; +using Polly.Telemetry; +using Xunit; + +namespace Polly.Core.Tests.Telemetry; + +public class NullResilienceTelemetryFactoryTests +{ + [Fact] + public void Instance_NotNull() + { + NullResilienceTelemetry.Instance.Should().NotBeNull(); + } + + [Fact] + public void Default_Ok() + { + var context = new ResilienceTelemetryFactoryContext(); + + context.BuilderName.Should().BeEmpty(); + context.StrategyType.Should().BeEmpty(); + context.StrategyName.Should().BeEmpty(); + context.BuilderProperties.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryTests.cs b/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryTests.cs new file mode 100644 index 00000000000..32bc24e317f --- /dev/null +++ b/src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryTests.cs @@ -0,0 +1,29 @@ +using System; +using FluentAssertions; +using Polly.Telemetry; +using Xunit; + +namespace Polly.Core.Tests.Telemetry; + +public class NullResilienceTelemetryTests +{ + [Fact] + public void Instance_NotNull() + { + NullResilienceTelemetry.Instance.Should().NotBeNull(); + } + + [Fact] + public void Report_ShouldNotThrow() + { + NullResilienceTelemetry.Instance + .Invoking(v => + { + NullResilienceTelemetry.Instance.Report("dummy", ResilienceContext.Get()); + NullResilienceTelemetry.Instance.Report("dummy", 10, ResilienceContext.Get()); + NullResilienceTelemetry.Instance.ReportException("dummy", new InvalidOperationException(), ResilienceContext.Get()); + }) + .Should() + .NotThrow(); + } +} diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs index bf3622975e5..abb89f72955 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilder.cs @@ -1,3 +1,5 @@ +using Polly.Telemetry; + namespace Polly.Builder; /// @@ -88,11 +90,22 @@ public ResilienceStrategy Build() private ResilienceStrategy CreateResilienceStrategy(Entry entry) { - var context = new ResilienceStrategyBuilderContext( - builderName: Options.BuilderName, - builderProperties: Options.Properties, - strategyName: entry.Properties.StrategyName, - strategyType: entry.Properties.StrategyType); + var telemetryContext = new ResilienceTelemetryFactoryContext + { + BuilderName = Options.BuilderName, + BuilderProperties = Options.Properties, + StrategyName = entry.Properties.StrategyName, + StrategyType = entry.Properties.StrategyType + }; + + var context = new ResilienceStrategyBuilderContext + { + BuilderName = Options.BuilderName, + BuilderProperties = Options.Properties, + StrategyName = entry.Properties.StrategyName, + StrategyType = entry.Properties.StrategyType, + Telemetry = Options.TelemetryFactory.Create(telemetryContext) + }; return entry.Factory(context); } diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs index b4afb691377..bc059d9e6e8 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs @@ -1,3 +1,5 @@ +using Polly.Telemetry; + namespace Polly.Builder; /// @@ -5,35 +7,28 @@ namespace Polly.Builder; /// public class ResilienceStrategyBuilderContext { - internal ResilienceStrategyBuilderContext( - string builderName, - ResilienceProperties builderProperties, - string strategyName, - string strategyType) - { - BuilderName = Guard.NotNull(builderName); - BuilderProperties = Guard.NotNull(builderProperties); - StrategyName = Guard.NotNull(strategyName); - StrategyType = Guard.NotNull(strategyType); - } - /// /// Gets the name of the builder. /// - public string BuilderName { get; } + public string BuilderName { get; internal set; } = string.Empty; /// /// Gets the custom properties attached to the builder. /// - public ResilienceProperties BuilderProperties { get; } + public ResilienceProperties BuilderProperties { get; internal set; } = new(); /// /// Gets the name of the strategy. /// - public string StrategyName { get; } + public string StrategyName { get; internal set; } = string.Empty; /// /// Gets the type of the strategy. /// - public string StrategyType { get; } + public string StrategyType { get; internal set; } = string.Empty; + + /// + /// Gets the resilience telemetry used to report important events. + /// + public ResilienceTelemetry Telemetry { get; internal set; } = NullResilienceTelemetry.Instance; } diff --git a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs index b3e745a36d9..416b3cf3561 100644 --- a/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs +++ b/src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Polly.Telemetry; namespace Polly.Builder; @@ -18,4 +19,10 @@ public class ResilienceStrategyBuilderOptions /// Gets the custom properties attached to builder options. /// public ResilienceProperties Properties { get; } = new(); + + /// + /// Gets or sets an instance of . + /// + [Required] + public ResilienceTelemetryFactory TelemetryFactory { get; set; } = NullResilienceTelemetryFactory.Instance; } diff --git a/src/Polly.Core/Telemetry/NullResilienceTelemetry.cs b/src/Polly.Core/Telemetry/NullResilienceTelemetry.cs new file mode 100644 index 00000000000..a295bac3aee --- /dev/null +++ b/src/Polly.Core/Telemetry/NullResilienceTelemetry.cs @@ -0,0 +1,31 @@ +namespace Polly.Telemetry; + +/// +/// A implementation of that does nothing. +/// +public sealed class NullResilienceTelemetry : ResilienceTelemetry +{ + private NullResilienceTelemetry() + { + } + + /// + /// Gets an instance of . + /// + public static readonly NullResilienceTelemetry Instance = new(); + + /// + public override void Report(string eventName, ResilienceContext context) + { + } + + /// + public override void Report(string eventName, TResult result, ResilienceContext context) + { + } + + /// + public override void ReportException(string eventName, Exception exception, ResilienceContext context) + { + } +} \ No newline at end of file diff --git a/src/Polly.Core/Telemetry/NullResilienceTelemetryFactory.cs b/src/Polly.Core/Telemetry/NullResilienceTelemetryFactory.cs new file mode 100644 index 00000000000..263d673fdca --- /dev/null +++ b/src/Polly.Core/Telemetry/NullResilienceTelemetryFactory.cs @@ -0,0 +1,19 @@ +namespace Polly.Telemetry; + +/// +/// Factory that returns instances. +/// +public sealed class NullResilienceTelemetryFactory : ResilienceTelemetryFactory +{ + /// + /// Gets the singleton instance of the factory. + /// + public static readonly NullResilienceTelemetryFactory Instance = new(); + + private NullResilienceTelemetryFactory() + { + } + + /// + public override ResilienceTelemetry Create(ResilienceTelemetryFactoryContext context) => NullResilienceTelemetry.Instance; +} diff --git a/src/Polly.Core/Telemetry/ResilienceTelemetry.cs b/src/Polly.Core/Telemetry/ResilienceTelemetry.cs new file mode 100644 index 00000000000..cb6c58f5a6e --- /dev/null +++ b/src/Polly.Core/Telemetry/ResilienceTelemetry.cs @@ -0,0 +1,36 @@ +namespace Polly.Telemetry; + +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods + +/// +/// Resilience telemetry is used by individual resilience strategies to report some important events. +/// +/// +/// For example, the timeout strategy reports "OnTimeout" event when the timeout is reached or "OnRetry" for retry strategy. +/// +public abstract class ResilienceTelemetry +{ + /// + /// Reports an event that occurred in the resilience strategy. + /// + /// The event name. + /// The context associated with the event. + public abstract void Report(string eventName, ResilienceContext context); + + /// + /// Reports an event that occurred in the resilience strategy. + /// + /// The type of the result. + /// The event name. + /// The result associated with the event. + /// The context associated with the event. + public abstract void Report(string eventName, TResult result, ResilienceContext context); + + /// + /// Reports an event that occurred in the resilience strategy. + /// + /// The event name. + /// The exception associated with the event. + /// The context associated with the event. + public abstract void ReportException(string eventName, Exception exception, ResilienceContext context); +} diff --git a/src/Polly.Core/Telemetry/ResilienceTelemetryFactory.cs b/src/Polly.Core/Telemetry/ResilienceTelemetryFactory.cs new file mode 100644 index 00000000000..fd4139254fd --- /dev/null +++ b/src/Polly.Core/Telemetry/ResilienceTelemetryFactory.cs @@ -0,0 +1,16 @@ +namespace Polly.Telemetry; + +#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods + +/// +/// Factory used to created instances of . +/// +public abstract class ResilienceTelemetryFactory +{ + /// + /// Creates a new instance of . + /// + /// The context associated with the creation of . + /// An instance of . + public abstract ResilienceTelemetry Create(ResilienceTelemetryFactoryContext context); +} diff --git a/src/Polly.Core/Telemetry/ResilienceTelemetryFactoryContext.cs b/src/Polly.Core/Telemetry/ResilienceTelemetryFactoryContext.cs new file mode 100644 index 00000000000..9f516a30c5b --- /dev/null +++ b/src/Polly.Core/Telemetry/ResilienceTelemetryFactoryContext.cs @@ -0,0 +1,27 @@ +namespace Polly.Telemetry; + +/// +/// The context used for building an instance of . +/// +public class ResilienceTelemetryFactoryContext +{ + /// + /// Gets the name of the builder. + /// + public string BuilderName { get; internal set; } = string.Empty; + + /// + /// Gets the name of the strategy. + /// + public string StrategyName { get; internal set; } = string.Empty; + + /// + /// Gets the type of the strategy. + /// + public string StrategyType { get; internal set; } = string.Empty; + + /// + /// Gets the custom properties attached to the builder. + /// + public ResilienceProperties BuilderProperties { get; internal set; } = new(); +}