Skip to content

Commit

Permalink
Introduce ResilienceTelemetryFactory (App-vNext#1073)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Mar 21, 2023
1 parent 8967949 commit 4c1cd6e
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
45 changes: 45 additions & 0 deletions src/Polly.Core.Tests/Builder/ResilienceStrategyBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -288,6 +294,45 @@ public void BuildStrategy_EnsureCorrectContext()
verified2.Should().BeTrue();
}

[Fact]
public void BuildStrategy_EnsureTelemetryFactoryInvoked()
{
// arrange
var factory = new Mock<ResilienceTelemetryFactory>(MockBehavior.Strict);
var builder = new ResilienceStrategyBuilder
{
Options = new ResilienceStrategyBuilderOptions
{
BuilderName = "builder-name",
TelemetryFactory = factory.Object
},
};

factory
.Setup(v => v.Create(It.IsAny<ResilienceTelemetryFactoryContext>()))
.Returns(NullResilienceTelemetry.Instance)
.Callback<ResilienceTelemetryFactoryContext>(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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
29 changes: 29 additions & 0 deletions src/Polly.Core.Tests/Telemetry/NullResilienceTelemetryTests.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
23 changes: 18 additions & 5 deletions src/Polly.Core/Builder/ResilienceStrategyBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Polly.Telemetry;

namespace Polly.Builder;

/// <summary>
Expand Down Expand Up @@ -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);
}
Expand Down
27 changes: 11 additions & 16 deletions src/Polly.Core/Builder/ResilienceStrategyBuilderContext.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
using Polly.Telemetry;

namespace Polly.Builder;

/// <summary>
/// The context used for building an individual resilience strategy.
/// </summary>
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);
}

/// <summary>
/// Gets the name of the builder.
/// </summary>
public string BuilderName { get; }
public string BuilderName { get; internal set; } = string.Empty;

/// <summary>
/// Gets the custom properties attached to the builder.
/// </summary>
public ResilienceProperties BuilderProperties { get; }
public ResilienceProperties BuilderProperties { get; internal set; } = new();

/// <summary>
/// Gets the name of the strategy.
/// </summary>
public string StrategyName { get; }
public string StrategyName { get; internal set; } = string.Empty;

/// <summary>
/// Gets the type of the strategy.
/// </summary>
public string StrategyType { get; }
public string StrategyType { get; internal set; } = string.Empty;

/// <summary>
/// Gets the resilience telemetry used to report important events.
/// </summary>
public ResilienceTelemetry Telemetry { get; internal set; } = NullResilienceTelemetry.Instance;
}
7 changes: 7 additions & 0 deletions src/Polly.Core/Builder/ResilienceStrategyBuilderOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Polly.Telemetry;

namespace Polly.Builder;

Expand All @@ -18,4 +19,10 @@ public class ResilienceStrategyBuilderOptions
/// Gets the custom properties attached to builder options.
/// </summary>
public ResilienceProperties Properties { get; } = new();

/// <summary>
/// Gets or sets an instance of <see cref="TelemetryFactory"/>.
/// </summary>
[Required]
public ResilienceTelemetryFactory TelemetryFactory { get; set; } = NullResilienceTelemetryFactory.Instance;
}
31 changes: 31 additions & 0 deletions src/Polly.Core/Telemetry/NullResilienceTelemetry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Polly.Telemetry;

/// <summary>
/// A implementation of <see cref="ResilienceTelemetryFactory"/> that does nothing.
/// </summary>
public sealed class NullResilienceTelemetry : ResilienceTelemetry
{
private NullResilienceTelemetry()
{
}

/// <summary>
/// Gets an instance of <see cref="NullResilienceTelemetry"/>.
/// </summary>
public static readonly NullResilienceTelemetry Instance = new();

/// <inheritdoc/>
public override void Report(string eventName, ResilienceContext context)
{
}

/// <inheritdoc/>
public override void Report<TResult>(string eventName, TResult result, ResilienceContext context)
{
}

/// <inheritdoc/>
public override void ReportException(string eventName, Exception exception, ResilienceContext context)
{
}
}
19 changes: 19 additions & 0 deletions src/Polly.Core/Telemetry/NullResilienceTelemetryFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Polly.Telemetry;

/// <summary>
/// Factory that returns <see cref="NullResilienceTelemetry"/> instances.
/// </summary>
public sealed class NullResilienceTelemetryFactory : ResilienceTelemetryFactory
{
/// <summary>
/// Gets the singleton instance of the factory.
/// </summary>
public static readonly NullResilienceTelemetryFactory Instance = new();

private NullResilienceTelemetryFactory()
{
}

/// <inheritdoc/>
public override ResilienceTelemetry Create(ResilienceTelemetryFactoryContext context) => NullResilienceTelemetry.Instance;
}
36 changes: 36 additions & 0 deletions src/Polly.Core/Telemetry/ResilienceTelemetry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace Polly.Telemetry;

#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods

/// <summary>
/// Resilience telemetry is used by individual resilience strategies to report some important events.
/// </summary>
/// <remarks>
/// For example, the timeout strategy reports "OnTimeout" event when the timeout is reached or "OnRetry" for retry strategy.
/// </remarks>
public abstract class ResilienceTelemetry
{
/// <summary>
/// Reports an event that occurred in the resilience strategy.
/// </summary>
/// <param name="eventName">The event name.</param>
/// <param name="context">The context associated with the event.</param>
public abstract void Report(string eventName, ResilienceContext context);

/// <summary>
/// Reports an event that occurred in the resilience strategy.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="eventName">The event name.</param>
/// <param name="result">The result associated with the event.</param>
/// <param name="context">The context associated with the event.</param>
public abstract void Report<TResult>(string eventName, TResult result, ResilienceContext context);

/// <summary>
/// Reports an event that occurred in the resilience strategy.
/// </summary>
/// <param name="eventName">The event name.</param>
/// <param name="exception">The exception associated with the event.</param>
/// <param name="context">The context associated with the event.</param>
public abstract void ReportException(string eventName, Exception exception, ResilienceContext context);
}
16 changes: 16 additions & 0 deletions src/Polly.Core/Telemetry/ResilienceTelemetryFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Polly.Telemetry;

#pragma warning disable S1694 // An abstract class should have both abstract and concrete methods

/// <summary>
/// Factory used to created instances of <see cref="ResilienceTelemetry"/>.
/// </summary>
public abstract class ResilienceTelemetryFactory
{
/// <summary>
/// Creates a new instance of <see cref="ResilienceTelemetry"/>.
/// </summary>
/// <param name="context">The context associated with the creation of <see cref="ResilienceTelemetry"/>.</param>
/// <returns>An instance of <see cref="ResilienceTelemetry"/>.</returns>
public abstract ResilienceTelemetry Create(ResilienceTelemetryFactoryContext context);
}
27 changes: 27 additions & 0 deletions src/Polly.Core/Telemetry/ResilienceTelemetryFactoryContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Polly.Telemetry;

/// <summary>
/// The context used for building an instance of <see cref="ResilienceTelemetry"/>.
/// </summary>
public class ResilienceTelemetryFactoryContext
{
/// <summary>
/// Gets the name of the builder.
/// </summary>
public string BuilderName { get; internal set; } = string.Empty;

/// <summary>
/// Gets the name of the strategy.
/// </summary>
public string StrategyName { get; internal set; } = string.Empty;

/// <summary>
/// Gets the type of the strategy.
/// </summary>
public string StrategyType { get; internal set; } = string.Empty;

/// <summary>
/// Gets the custom properties attached to the builder.
/// </summary>
public ResilienceProperties BuilderProperties { get; internal set; } = new();
}

0 comments on commit 4c1cd6e

Please sign in to comment.