Skip to content

Commit 0f66d3e

Browse files
authored
Allow changing the severity of resilience events (#2072)
1 parent f85029c commit 0f66d3e

File tree

8 files changed

+227
-13
lines changed

8 files changed

+227
-13
lines changed

docs/advanced/telemetry.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,31 @@ Each resilience strategy can generate telemetry data through the [`ResilienceStr
255255
To leverage this telemetry data, users should assign a `TelemetryListener` instance to `ResiliencePipelineBuilder.TelemetryListener` and then consume the `TelemetryEventArguments`.
256256

257257
For common scenarios, it is expected that users would make use of `Polly.Extensions`. This extension enables telemetry configuration through the `ResiliencePipelineBuilder.ConfigureTelemetry(...)` method, which processes `TelemetryEventArguments` to generate logs and metrics.
258+
259+
## Customizing the severity of telemetry events
260+
261+
To customize the severity of telemetry events, set the [`SeverityProvider`](xref:Polly.Telemetry.TelemetryOptions.SeverityProvider) delegate that allows changing the default severity of resilience events:
262+
263+
<!-- snippet: telemetry-severity-override -->
264+
```cs
265+
services.AddResiliencePipeline("my-strategy", (builder, context) =>
266+
{
267+
// Create a new instance of telemetry options by using copy-constructor of the global ones.
268+
// This ensures that common configuration is preserved.
269+
var telemetryOptions = new TelemetryOptions(context.GetOptions<TelemetryOptions>());
270+
271+
telemetryOptions.SeverityProvider = args => args.Event.EventName switch
272+
{
273+
// Decrease severity of specific events
274+
"OnRetry" => ResilienceEventSeverity.Debug,
275+
"ExecutionAttempt" => ResilienceEventSeverity.Debug,
276+
_ => args.Event.Severity
277+
};
278+
279+
builder.AddRetry(new RetryStrategyOptions());
280+
281+
// Override the telemetry configuration for this pipeline.
282+
builder.ConfigureTelemetry(telemetryOptions);
283+
});
284+
```
285+
<!-- endSnippet -->

src/Polly.Extensions/PublicAPI.Unshipped.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
#nullable enable
2+
Polly.Telemetry.SeverityProviderArguments
3+
Polly.Telemetry.SeverityProviderArguments.Context.get -> Polly.ResilienceContext!
4+
Polly.Telemetry.SeverityProviderArguments.Event.get -> Polly.Telemetry.ResilienceEvent
5+
Polly.Telemetry.SeverityProviderArguments.SeverityProviderArguments() -> void
6+
Polly.Telemetry.SeverityProviderArguments.SeverityProviderArguments(Polly.Telemetry.ResilienceTelemetrySource! source, Polly.Telemetry.ResilienceEvent resilienceEvent, Polly.ResilienceContext! context) -> void
7+
Polly.Telemetry.SeverityProviderArguments.Source.get -> Polly.Telemetry.ResilienceTelemetrySource!
8+
Polly.Telemetry.TelemetryOptions.SeverityProvider.get -> System.Func<Polly.Telemetry.SeverityProviderArguments, Polly.Telemetry.ResilienceEventSeverity>?
9+
Polly.Telemetry.TelemetryOptions.SeverityProvider.set -> void
10+
Polly.Telemetry.TelemetryOptions.TelemetryOptions(Polly.Telemetry.TelemetryOptions! other) -> void
211
static Polly.PollyServiceCollectionExtensions.AddResiliencePipelines<TKey>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>!>! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
312
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>
413
Polly.DependencyInjection.AddResiliencePipelinesContext<TKey>.AddResiliencePipeline(TKey key, System.Action<Polly.ResiliencePipelineBuilder!, Polly.DependencyInjection.AddResiliencePipelineContext<TKey>!>! configure) -> void
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Polly.Telemetry;
2+
3+
#pragma warning disable CA1815 // Override equals and operator equals on value types
4+
5+
/// <summary>
6+
/// Arguments used by <see cref="TelemetryOptions.SeverityProvider"/>.
7+
/// </summary>
8+
public readonly struct SeverityProviderArguments
9+
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="SeverityProviderArguments"/> struct.
12+
/// </summary>
13+
/// <param name="source">The source that produced the resilience event.</param>
14+
/// <param name="resilienceEvent">The resilience event.</param>
15+
/// <param name="context">The resilience context.</param>
16+
public SeverityProviderArguments(ResilienceTelemetrySource source, ResilienceEvent resilienceEvent, ResilienceContext context)
17+
{
18+
Source = source;
19+
Event = resilienceEvent;
20+
Context = context;
21+
}
22+
23+
/// <summary>
24+
/// Gets the source that produced the resilience event.
25+
/// </summary>
26+
public ResilienceTelemetrySource Source { get; }
27+
28+
/// <summary>
29+
/// Gets the resilience event.
30+
/// </summary>
31+
public ResilienceEvent Event { get; }
32+
33+
/// <summary>
34+
/// Gets the resilience context.
35+
/// </summary>
36+
public ResilienceContext Context { get; }
37+
}

src/Polly.Extensions/Telemetry/TelemetryListenerImpl.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal sealed class TelemetryListenerImpl : TelemetryListener
1111
private readonly ILogger _logger;
1212
private readonly Func<ResilienceContext, object?, object?> _resultFormatter;
1313
private readonly List<TelemetryListener> _listeners;
14+
private readonly Func<SeverityProviderArguments, ResilienceEventSeverity>? _severityProvider;
1415
private readonly List<MeteringEnricher> _enrichers;
1516

1617
public TelemetryListenerImpl(TelemetryOptions options)
@@ -19,6 +20,7 @@ public TelemetryListenerImpl(TelemetryOptions options)
1920
_logger = options.LoggerFactory.CreateLogger(TelemetryUtil.PollyDiagnosticSource);
2021
_resultFormatter = options.ResultFormatter;
2122
_listeners = options.TelemetryListeners.ToList();
23+
_severityProvider = options.SeverityProvider;
2224

2325
Counter = Meter.CreateCounter<int>(
2426
"resilience.polly.strategy.events",
@@ -52,8 +54,15 @@ public override void Write<TResult, TArgs>(in TelemetryEventArguments<TResult, T
5254
}
5355
}
5456

55-
LogEvent(in args);
56-
MeterEvent(in args);
57+
var severity = args.Event.Severity;
58+
59+
if (_severityProvider is { } provider)
60+
{
61+
severity = provider(new SeverityProviderArguments(args.Source, args.Event, args.Context));
62+
}
63+
64+
LogEvent(in args, severity);
65+
MeterEvent(in args, severity);
5766
}
5867

5968
private static bool GetArgs<T, TArgs>(T inArgs, out TArgs outArgs)
@@ -68,13 +77,13 @@ private static bool GetArgs<T, TArgs>(T inArgs, out TArgs outArgs)
6877
return false;
6978
}
7079

71-
private static void AddCommonTags<TResult, TArgs>(in EnrichmentContext<TResult, TArgs> context)
80+
private static void AddCommonTags<TResult, TArgs>(in EnrichmentContext<TResult, TArgs> context, ResilienceEventSeverity severity)
7281
{
7382
var source = context.TelemetryEvent.Source;
7483
var ev = context.TelemetryEvent.Event;
7584

7685
context.Tags.Add(new(ResilienceTelemetryTags.EventName, context.TelemetryEvent.Event.EventName));
77-
context.Tags.Add(new(ResilienceTelemetryTags.EventSeverity, context.TelemetryEvent.Event.Severity.AsString()));
86+
context.Tags.Add(new(ResilienceTelemetryTags.EventSeverity, severity.AsString()));
7887

7988
if (source.PipelineName is not null)
8089
{
@@ -102,7 +111,7 @@ private static void AddCommonTags<TResult, TArgs>(in EnrichmentContext<TResult,
102111
}
103112
}
104113

105-
private void MeterEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArgs> args)
114+
private void MeterEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArgs> args, ResilienceEventSeverity severity)
106115
{
107116
var arguments = args.Arguments;
108117

@@ -115,7 +124,7 @@ private void MeterEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArg
115124

116125
var tags = TagsList.Get();
117126
var context = new EnrichmentContext<TResult, TArgs>(in args, tags.Tags);
118-
UpdateEnrichmentContext(in context);
127+
UpdateEnrichmentContext(in context, severity);
119128
ExecutionDuration.Record(executionFinished.Duration.TotalMilliseconds, tags.TagsSpan);
120129
TagsList.Return(tags);
121130
}
@@ -128,7 +137,7 @@ private void MeterEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArg
128137

129138
var tags = TagsList.Get();
130139
var context = new EnrichmentContext<TResult, TArgs>(in args, tags.Tags);
131-
UpdateEnrichmentContext(in context);
140+
UpdateEnrichmentContext(in context, severity);
132141
context.Tags.Add(new(ResilienceTelemetryTags.AttemptNumber, executionAttempt.AttemptNumber.AsBoxedInt()));
133142
context.Tags.Add(new(ResilienceTelemetryTags.AttemptHandled, executionAttempt.Handled.AsBoxedBool()));
134143
AttemptDuration.Record(executionAttempt.Duration.TotalMilliseconds, tags.TagsSpan);
@@ -138,15 +147,15 @@ private void MeterEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArg
138147
{
139148
var tags = TagsList.Get();
140149
var context = new EnrichmentContext<TResult, TArgs>(in args, tags.Tags);
141-
UpdateEnrichmentContext(in context);
150+
UpdateEnrichmentContext(in context, severity);
142151
Counter.Add(1, tags.TagsSpan);
143152
TagsList.Return(tags);
144153
}
145154
}
146155

147-
private void UpdateEnrichmentContext<TResult, TArgs>(in EnrichmentContext<TResult, TArgs> context)
156+
private void UpdateEnrichmentContext<TResult, TArgs>(in EnrichmentContext<TResult, TArgs> context, ResilienceEventSeverity severity)
148157
{
149-
AddCommonTags(in context);
158+
AddCommonTags(in context, severity);
150159

151160
if (_enrichers.Count != 0)
152161
{
@@ -157,10 +166,10 @@ private void UpdateEnrichmentContext<TResult, TArgs>(in EnrichmentContext<TResul
157166
}
158167
}
159168

160-
private void LogEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArgs> args)
169+
private void LogEvent<TResult, TArgs>(in TelemetryEventArguments<TResult, TArgs> args, ResilienceEventSeverity severity)
161170
{
162171
var result = GetResult(args.Context, args.Outcome);
163-
var level = args.Event.Severity.AsLogLevel();
172+
var level = severity.AsLogLevel();
164173

165174
if (GetArgs<TArgs, PipelineExecutingArguments>(args.Arguments, out _))
166175
{

src/Polly.Extensions/Telemetry/TelemetryOptions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http;
33
using Microsoft.Extensions.Logging;
44
using Microsoft.Extensions.Logging.Abstractions;
5+
using Polly.Utils;
56

67
namespace Polly.Telemetry;
78

@@ -10,6 +11,28 @@ namespace Polly.Telemetry;
1011
/// </summary>
1112
public class TelemetryOptions
1213
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="TelemetryOptions"/> class.
16+
/// </summary>
17+
public TelemetryOptions()
18+
{
19+
}
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="TelemetryOptions"/> class.
23+
/// </summary>
24+
/// <param name="other">The telemetry options instance to copy the data from.</param>
25+
public TelemetryOptions(TelemetryOptions other)
26+
{
27+
Guard.NotNull(other);
28+
29+
TelemetryListeners = other.TelemetryListeners.ToList();
30+
LoggerFactory = other.LoggerFactory;
31+
MeteringEnrichers = other.MeteringEnrichers.ToList();
32+
ResultFormatter = other.ResultFormatter;
33+
SeverityProvider = other.SeverityProvider;
34+
}
35+
1336
/// <summary>
1437
/// Gets the collection of telemetry listeners.
1538
/// </summary>
@@ -48,4 +71,12 @@ public class TelemetryOptions
4871
HttpResponseMessage response => (int)response.StatusCode,
4972
_ => result,
5073
};
74+
75+
/// <summary>
76+
/// Gets or sets the resilience event severity provider that allows customizing the severity of resilience events.
77+
/// </summary>
78+
/// <value>
79+
/// The default value is <see langword="null"/>.
80+
/// </value>
81+
public Func<SeverityProviderArguments, ResilienceEventSeverity>? SeverityProvider { get; set; }
5182
}

src/Snippets/Docs/Telemetry.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,33 @@ public override void Enrich<TResult, TArgs>(in EnrichmentContext<TResult, TArgs>
134134
}
135135

136136
#endregion
137+
138+
public static void SeverityOverrides()
139+
{
140+
var services = new ServiceCollection();
141+
142+
#region telemetry-severity-override
143+
144+
services.AddResiliencePipeline("my-strategy", (builder, context) =>
145+
{
146+
// Create a new instance of telemetry options by using copy-constructor of the global ones.
147+
// This ensures that common configuration is preserved.
148+
var telemetryOptions = new TelemetryOptions(context.GetOptions<TelemetryOptions>());
149+
150+
telemetryOptions.SeverityProvider = args => args.Event.EventName switch
151+
{
152+
// Decrease severity of specific events
153+
"OnRetry" => ResilienceEventSeverity.Debug,
154+
"ExecutionAttempt" => ResilienceEventSeverity.Debug,
155+
_ => args.Event.Severity
156+
};
157+
158+
builder.AddRetry(new RetryStrategyOptions());
159+
160+
// Override the telemetry configuration for this pipeline.
161+
builder.ConfigureTelemetry(telemetryOptions);
162+
});
163+
164+
#endregion
165+
}
137166
}

test/Polly.Extensions.Tests/Telemetry/TelemetryListenerImplTests.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,45 @@ public void PipelineExecuted_ShouldBeSkipped()
453453
events.Should().HaveCount(0);
454454
}
455455

456+
[Fact]
457+
public void SeverityProvider_NotSet_SeverityRespected()
458+
{
459+
var telemetry = Create();
460+
461+
ReportEvent(telemetry, null, severity: ResilienceEventSeverity.Critical);
462+
463+
var records = _logger.GetRecords();
464+
465+
records.Single().LogLevel.Should().Be(LogLevel.Critical);
466+
}
467+
468+
[Fact]
469+
public void SeverityProvider_EnsureRespected()
470+
{
471+
var called = false;
472+
var telemetry = Create(configure: options =>
473+
{
474+
options.SeverityProvider = args =>
475+
{
476+
args.Event.Severity.Should().Be(ResilienceEventSeverity.Warning);
477+
args.Source.Should().NotBeNull();
478+
args.Context.Should().NotBeNull();
479+
called = true;
480+
return ResilienceEventSeverity.Critical;
481+
};
482+
});
483+
484+
ReportEvent(telemetry, null, severity: ResilienceEventSeverity.Warning);
485+
486+
var records = _logger.GetRecords();
487+
488+
records.Single().LogLevel.Should().Be(LogLevel.Critical);
489+
called.Should().BeTrue();
490+
}
491+
456492
private List<Dictionary<string, object?>> GetEvents(string eventName) => _events.Where(e => e.Name == eventName).Select(v => v.Tags).ToList();
457493

458-
private TelemetryListenerImpl Create(IEnumerable<MeteringEnricher>? enrichers = null)
494+
private TelemetryListenerImpl Create(IEnumerable<MeteringEnricher>? enrichers = null, Action<TelemetryOptions>? configure = null)
459495
{
460496
var options = new TelemetryOptions
461497
{
@@ -475,6 +511,8 @@ private TelemetryListenerImpl Create(IEnumerable<MeteringEnricher>? enrichers =
475511
}
476512
}
477513

514+
configure?.Invoke(options);
515+
478516
return new(options);
479517
}
480518

test/Polly.Extensions.Tests/Telemetry/TelemetryOptionsTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Net;
22
using System.Net.Http;
3+
using Microsoft.Extensions.Logging;
34
using Microsoft.Extensions.Logging.Abstractions;
5+
using NSubstitute;
46
using Polly.Telemetry;
57

68
namespace Polly.Extensions.Tests.Telemetry;
@@ -21,4 +23,35 @@ public void Ctor_EnsureDefaults()
2123
using var response = new HttpResponseMessage(HttpStatusCode.OK);
2224
options.ResultFormatter(resilienceContext, response).Should().Be(200);
2325
}
26+
27+
[Fact]
28+
public void CopyCtor_Ok()
29+
{
30+
var options = new TelemetryOptions
31+
{
32+
LoggerFactory = Substitute.For<ILoggerFactory>(),
33+
SeverityProvider = _ => ResilienceEventSeverity.Error,
34+
ResultFormatter = (_, _) => "x",
35+
};
36+
37+
options.MeteringEnrichers.Add(Substitute.For<MeteringEnricher>());
38+
options.TelemetryListeners.Add(Substitute.For<TelemetryListener>());
39+
40+
var other = new TelemetryOptions(options);
41+
42+
other.ResultFormatter.Should().Be(options.ResultFormatter);
43+
other.LoggerFactory.Should().Be(options.LoggerFactory);
44+
other.SeverityProvider.Should().Be(options.SeverityProvider);
45+
other.MeteringEnrichers.Should().BeEquivalentTo(options.MeteringEnrichers);
46+
other.TelemetryListeners.Should().BeEquivalentTo(options.TelemetryListeners);
47+
other.TelemetryListeners.Should().NotBeSameAs(options.TelemetryListeners);
48+
other.MeteringEnrichers.Should().NotBeSameAs(options.MeteringEnrichers);
49+
50+
typeof(TelemetryOptions).GetRuntimeProperties().Should().HaveCount(5);
51+
}
52+
53+
[Fact]
54+
public void CopyCtor_Reminder()
55+
=> typeof(TelemetryOptions).GetRuntimeProperties().Should()
56+
.HaveCount(5, "Make sure that when you increase this number, you also update the copy constructor to assign the new property.");
2457
}

0 commit comments

Comments
 (0)