Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resilience telemetry API #1138

Merged
merged 1 commit into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ tab_width = 2
[*.cs]

# Put any C# specific settings here
dotnet_code_quality.CA1062.null_check_validation_methods = NotNull
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ updates:
- dependency-name: "System.ValueTuple"
- dependency-name: "Microsoft.Extensions.Options"
- dependency-name: "Microsoft.Extensions.Logging.Abstractions"
- dependency-name: "Microsoft.Extensions.Logging"
- dependency-name: "System.Diagnostics.DiagnosticSource"
- dependency-name: "System.Threading.RateLimiting"
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
martincostello marked this conversation as resolved.
Show resolved Hide resolved
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="Moq" Version="4.18.4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Polly.Registry;

namespace Polly.Core.Tests.Registry;
public class ResilienceStrategyRegistryOptionsTests
{
[Fact]
public void Ctor_EnsureDefaults()
{
ResilienceStrategyRegistryOptions<object> options = new();

options.KeyFormatter.Should().NotBeNull();
options.KeyFormatter(null!).Should().Be("");
options.KeyFormatter("ABC").Should().Be("ABC");
}
}
19 changes: 19 additions & 0 deletions src/Polly.Core.Tests/Registry/ResilienceStrategyRegistryTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Polly.Registry;
using Polly.Telemetry;

namespace Polly.Core.Tests.Registry;

Expand Down Expand Up @@ -117,6 +118,24 @@ public void AddBuilder_GetStrategy_EnsureCalled()
strategies.Keys.Should().HaveCount(3);
}

[Fact]
public void AddBuilder_EnsureStrategyKey()
{
var called = false;
var registry = CreateRegistry();
registry.TryAddBuilder(StrategyId.Create("A"), (key, builder) =>
{
builder.AddStrategy(new TestResilienceStrategy());
builder.Properties.TryGetValue(TelemetryUtil.StrategyKey, out var val).Should().BeTrue();
val.Should().Be(key.ToString());
called = true;
});

registry.Get(StrategyId.Create("A", "Instance1"));

called.Should().BeTrue();
}

[Fact]
public void TryGet_NoBuilder_Null()
{
Expand Down
32 changes: 19 additions & 13 deletions src/Polly.Core.Tests/ResilienceContextTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ public void Get_EnsureDefaults()
}

[Fact]
public void Get_EnsurePooled()
public async Task Get_EnsurePooled()
{
var context = ResilienceContext.Get();
await TestUtils.AssertWithTimeoutAsync(() =>
{
var context = ResilienceContext.Get();

ResilienceContext.Return(context);
ResilienceContext.Return(context);

ResilienceContext.Get().Should().BeSameAs(context);
ResilienceContext.Get().Should().BeSameAs(context);
});
}

[Fact]
Expand All @@ -33,17 +36,20 @@ public void Return_Null_Throws()
}

[Fact]
public void Return_EnsureDefaults()
public async Task Return_EnsureDefaults()
{
using var cts = new CancellationTokenSource();
var context = ResilienceContext.Get();
context.CancellationToken = cts.Token;
context.Initialize<bool>(true);
context.CancellationToken.Should().Be(cts.Token);
context.Properties.Set(new ResiliencePropertyKey<int>("abc"), 10);
ResilienceContext.Return(context);
await TestUtils.AssertWithTimeoutAsync(() =>
{
using var cts = new CancellationTokenSource();
var context = ResilienceContext.Get();
context.CancellationToken = cts.Token;
context.Initialize<bool>(true);
context.CancellationToken.Should().Be(cts.Token);
context.Properties.Set(new ResiliencePropertyKey<int>("abc"), 10);
ResilienceContext.Return(context);

AssertDefaults(context);
AssertDefaults(context);
});
}

[InlineData(true)]
Expand Down
10 changes: 10 additions & 0 deletions src/Polly.Core.Tests/Strategy/ResilienceStrategyTelemetryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ public void Report_NoOutcomeWhenNotSubscribed_None()
_diagnosticSource.VerifyNoOtherCalls();
}

[Fact]
public void ResilienceStrategyTelemetry_NoDiagnosticSource_Ok()
{
var source = new ResilienceTelemetrySource("builder", new ResilienceProperties(), "strategy-name", "strategy-type");
var sut = new ResilienceStrategyTelemetry(source, null);

sut.Invoking(s => s.Report("dummy", new TestArguments())).Should().NotThrow();
sut.Invoking(s => s.Report("dummy", new Outcome<int>(1), new TestArguments())).Should().NotThrow();
}

[Fact]
public void Report_Outcome_OK()
{
Expand Down
11 changes: 8 additions & 3 deletions src/Polly.Core.Tests/Telemetry/TelemetryUtilTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ namespace Polly.Core.Tests.Telemetry;

public class TelemetryUtilTests
{
[Fact]
public void Ctor_Ok()
{
TelemetryUtil.DiagnosticSourceKey.Key.Should().Be("DiagnosticSource");
TelemetryUtil.StrategyKey.Key.Should().Be("StrategyKey");
}

[Fact]
public void CreateResilienceTelemetry_Ok()
{
Expand All @@ -14,9 +21,7 @@ public void CreateResilienceTelemetry_Ok()
telemetry.TelemetrySource.BuilderName.Should().Be("builder");
telemetry.TelemetrySource.StrategyName.Should().Be("strategy-name");
telemetry.TelemetrySource.StrategyType.Should().Be("strategy-type");
telemetry.DiagnosticSource.Should().NotBeNull();

telemetry.DiagnosticSource.Should().BeOfType<DiagnosticListener>().Subject.Name.Should().Be("Polly");
telemetry.DiagnosticSource.Should().BeNull();
}

[Fact]
Expand Down
1 change: 1 addition & 0 deletions src/Polly.Core/Polly.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<Using Include="Polly.Utils" />
<Using Remove="System.Net.Http" />
<InternalsVisibleToTest Include="Polly.Core.Tests" />
<InternalsVisibleToTest Include="Polly.Extensions" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/Polly.Core/Registry/ResilienceStrategyRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Polly.Telemetry;

namespace Polly.Registry;

Expand All @@ -20,6 +21,7 @@ public sealed class ResilienceStrategyRegistry<TKey> : ResilienceStrategyProvide
private readonly Func<ResilienceStrategyBuilder> _activator;
private readonly ConcurrentDictionary<TKey, Action<TKey, ResilienceStrategyBuilder>> _builders;
private readonly ConcurrentDictionary<TKey, ResilienceStrategy> _strategies;
private readonly Func<TKey, string> _keyFormatter;

/// <summary>
/// Initializes a new instance of the <see cref="ResilienceStrategyRegistry{TKey}"/> class with the default comparer.
Expand All @@ -42,6 +44,7 @@ public ResilienceStrategyRegistry(ResilienceStrategyRegistryOptions<TKey> option
_activator = options.BuilderFactory;
_builders = new ConcurrentDictionary<TKey, Action<TKey, ResilienceStrategyBuilder>>(options.BuilderComparer);
_strategies = new ConcurrentDictionary<TKey, ResilienceStrategy>(options.StrategyComparer);
_keyFormatter = options.KeyFormatter;
}

/// <summary>
Expand Down Expand Up @@ -86,6 +89,7 @@ public override bool TryGet(TKey key, [NotNullWhen(true)] out ResilienceStrategy
strategy = _strategies.GetOrAdd(key, key =>
{
var builder = _activator();
builder.Properties.Set(TelemetryUtil.StrategyKey, _keyFormatter(key));
configure(key, builder);
return builder.Build();
});
Expand Down
6 changes: 6 additions & 0 deletions src/Polly.Core/Registry/ResilienceStrategyRegistryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@ public class ResilienceStrategyRegistryOptions<TKey>
/// </remarks>
[Required]
public IEqualityComparer<TKey> BuilderComparer { get; set; } = EqualityComparer<TKey>.Default;

/// <summary>
/// Gets or sets the formatter that is used by the registry to format the keys as a string.
/// </summary>
[Required]
public Func<TKey, string> KeyFormatter { get; set; } = (key) => key?.ToString() ?? string.Empty;
}
8 changes: 4 additions & 4 deletions src/Polly.Core/Strategy/ResilienceStrategyTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ namespace Polly.Strategy;
/// </remarks>
public sealed class ResilienceStrategyTelemetry
{
internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, DiagnosticSource diagnosticSource)
internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, DiagnosticSource? diagnosticSource)
{
TelemetrySource = source;
DiagnosticSource = diagnosticSource;
}

internal DiagnosticSource DiagnosticSource { get; }
internal DiagnosticSource? DiagnosticSource { get; }

internal ResilienceTelemetrySource TelemetrySource { get; }

Expand All @@ -29,7 +29,7 @@ internal ResilienceStrategyTelemetry(ResilienceTelemetrySource source, Diagnosti
public void Report<TArgs>(string eventName, TArgs args)
where TArgs : IResilienceArguments
{
if (!DiagnosticSource.IsEnabled(eventName))
if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(eventName))
{
return;
}
Expand All @@ -48,7 +48,7 @@ public void Report<TArgs>(string eventName, TArgs args)
public void Report<TArgs, TResult>(string eventName, Outcome<TResult> outcome, TArgs args)
where TArgs : IResilienceArguments
{
if (!DiagnosticSource.IsEnabled(eventName))
if (DiagnosticSource is null || !DiagnosticSource.IsEnabled(eventName))
{
return;
}
Expand Down
12 changes: 4 additions & 8 deletions src/Polly.Core/Telemetry/TelemetryUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ namespace Polly.Telemetry;

internal static class TelemetryUtil
{
private const string PollyDiagnosticSource = "Polly";
internal const string PollyDiagnosticSource = "Polly";

private static readonly DiagnosticSource DefaultDiagnosticSource = new DiagnosticListener(PollyDiagnosticSource);
internal static readonly ResiliencePropertyKey<DiagnosticSource> DiagnosticSourceKey = new("DiagnosticSource");

private static readonly ResiliencePropertyKey<DiagnosticSource> DiagnosticSourceKey = new("DiagnosticSource");
internal static readonly ResiliencePropertyKey<string> StrategyKey = new("StrategyKey");

public static ResilienceStrategyTelemetry CreateTelemetry(string builderName, ResilienceProperties builderProperties, string strategyName, string strategyType)
{
// Allows the user to override the default diagnostic source.
if (!builderProperties.TryGetValue(DiagnosticSourceKey, out var diagnosticSource))
{
diagnosticSource = DefaultDiagnosticSource;
}
builderProperties.TryGetValue(DiagnosticSourceKey, out var diagnosticSource);

var telemetrySource = new ResilienceTelemetrySource(builderName, builderProperties, strategyName, strategyType);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Polly.Extensions.DependencyInjection;
using Polly.Extensions.Telemetry;
using Polly.Registry;
using Polly.Strategy;

Expand Down Expand Up @@ -67,6 +69,30 @@ public void AddResilienceStrategy_EnsureContextFilled()
asserted.Should().BeTrue();
}

[Fact]
public void AddResilienceStrategy_EnsureTelemetryEnabled()
{
ResilienceStrategyTelemetry? telemetry = null;

_services.AddLogging();
_services.AddResilienceStrategy(Key, context =>
{
context.Builder.AddStrategy(context =>
{
telemetry = context.Telemetry;
return new TestStrategy();
});
});

CreateProvider().Get(Key);

var diagSource = telemetry!.GetType().GetProperty("DiagnosticSource", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(telemetry);

diagSource
.Should().BeOfType<ResilienceTelemetryDiagnosticSource>().Subject.LoggerFactory
.Should().NotBe(NullLoggerFactory.Instance);
}

[Fact]
public void AddResilienceStrategy_EnsureResilienceStrategyBuilderResolvedCorrectly()
{
Expand Down
27 changes: 27 additions & 0 deletions src/Polly.Extensions.Tests/Helpers/FakeLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Logging;

namespace Polly.Extensions.Tests.Helpers;

#pragma warning disable CS8633 // Nullability in constraints for type parameter doesn't match the constraints for type parameter in implicitly implemented interface method'.

public class FakeLogger : ILogger
{
public List<string> Messages { get; } = new();

public List<Exception?> Exceptions { get; } = new();

public List<EventId> Events { get; } = new();

public IDisposable BeginScope<TState>(TState state)
where TState : notnull => throw new NotSupportedException();

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
where TState : notnull
{
Exceptions.Add(exception);
Messages.Add(formatter(state, exception));
Events.Add(eventId);
}
}
5 changes: 5 additions & 0 deletions src/Polly.Extensions.Tests/Helpers/TestArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Polly.Strategy;

namespace Polly.Extensions.Tests.Helpers;

public record class TestArguments(ResilienceContext Context) : IResilienceArguments;
43 changes: 43 additions & 0 deletions src/Polly.Extensions.Tests/Helpers/TestStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Polly.Strategy;

namespace Polly.Extensions.Tests.Helpers;

public class TestStrategy : ResilienceStrategy
{
private readonly ResilienceStrategyTelemetry _telemetry;
private readonly bool _noOutcome;

public TestStrategy(ResilienceStrategyTelemetry telemetry, bool noOutcome)
{
_telemetry = telemetry;
_noOutcome = noOutcome;
}

protected override async ValueTask<TResult> ExecuteCoreAsync<TResult, TState>(Func<ResilienceContext, TState, ValueTask<TResult>> callback, ResilienceContext context, TState state)
{
if (_noOutcome)
{
_telemetry.Report("no-outcome", new TestArguments(context));
}

try
{
var result = await callback(context, state);
if (!_noOutcome)
{
_telemetry.Report("outcome", new Outcome<TResult>(result), new TestArguments(context));
}

return result;
}
catch (Exception e)
{
if (!_noOutcome)
{
_telemetry.Report("outcome", new Outcome<TResult>(e), new TestArguments(context));
}

throw;
}
}
}
Loading