Terje
Terje

Reputation: 387

How to log retries from Polly with ILoggerFactory

Or: How to log from a static method.

From https://github.com/App-vNext/Polly you have examples like this one where a logger is magically available:

Policy
  .Timeout(30, onTimeout: (context, timespan, task) => 
    {
        logger.Warn($"{context.PolicyKey} at {context.ExecutionKey}: execution timed out after {timespan.TotalSeconds} seconds.");
    });

In my code I am using the new IHttpClientFactory pattern from dotnet core 2.1, and adding it like this in my Startup.cs ConfigureServices method:

    services.AddHttpClient<IMySuperHttpClient, MySuperHttpClient>()
        .AddPolicyHandler(MySuperHttpClient.GetRetryPolicy())
        .AddPolicyHandler(MySuperHttpClient.GetCircuitBreakerPolicy());

With the GetRetryPolicy being static and looking like this:

internal static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(
            retryCount: 4,
            sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            onRetry: OnRetry);
}

Where the OnRetry method aswell have to be static:

private static void OnRetry(DelegateResult<HttpResponseMessage> delegateResult, TimeSpan timespan, Context context)
{
    // var logger = ??
    // logger.LogWarning($"API call failed blah blah.");
}

How can you access an ILoggerFactory here, if at all possible?

Upvotes: 13

Views: 7444

Answers (1)

Peter Csala
Peter Csala

Reputation: 22829

As it was stated in the comments by mountain traveller there are multiple ways to solve the issue:

  • Use a Context to pass any arbitrary object between the Execute(Async) and onRetry(Async) delegate
  • Use a different overload of AddPolicyHandler

The former solution requires an explicit call of Execute / ExecuteAsync to be able to provide a Context object which encapsulates an ILogger instance. In case of Named/Typed Http Client this route is not a viable option, since you decorate the whole HttpClient with a PolicyHttpMessageHandler. So, not your code is the one which calls the Execute(Async) rather the Handler on your behalf.

The latter solution provides access to the IServiceProvider from which you can retrieve any registered service. (It also passes the request as a parameter.)

services.AddHttpClient<IMySuperHttpClient, MySuperHttpClient>()
        .AddPolicyHandler((provider, _) => MySuperHttpClient.GetRetryPolicy(provider))

So, all you need to do is to pass through the IServiceProvider down to your OnRetry method

internal static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(IServiceProvider provider)
=> HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == HttpStatusCode.NotFound)
        .WaitAndRetryAsync(4,
            retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            OnRetry(provider));
}

There is no such overload of WaitAndRetryAsync where the onRetry delegate anticipates an IServiceProvider. We need to introduce a helper method like this:

private static Action<DelegateResult<HttpResponseMessage>, TimeSpan> OnRetry(IServiceProvider provider)
    => (dr, ts) => OnRetry(dr, ts, provider);

private static void OnRetry(DelegateResult<HttpResponseMessage> delegateResult, TimeSpan timespan, IServiceProvider provider)
{
    // var logger = ??
    // logger.LogWarning($"API call failed blah blah.");
}
  • The first OnRetry receives an IServiceProvider and returns an Action<DelegateResult<HttpResponseMessage>, TimeSpan>
    • The returned method is a good delegate for the WaitAndRetryAsync's onRetry
  • The second OnRetry is the same yours, I've just replaced your context parameter to the provider

So, basically the first OnRetry acts as an adapter between the WaitAndRetryAsync and the second OnRetry to pass through the provider.

Upvotes: 9

Related Questions