Dr. Strangelove
Dr. Strangelove

Reputation: 3328

Pass ILogger<T> to Polly retry policy without HttpRequestMessage

When making HTTP calls using an instance of HttpClient wrapped in Polly's retry policy and injected into a controller using dependency injection, I want to send ILogger<T> from the call site to a delegate of the retry policy (e.g., onRetry), so logs are registered more appropriately.

Polly docs explain how to achieve this by sending ILogger<T> from the calls site to the retry delegates leveraging Context encapsulated in an HttpRequestMessage request.

However, this solution works when you leverage a method of the HttpClient that takes HttpRequestMessage in one of its overloads. For instance, client.SendAsync.

However, not every method of HttpClient take HttpRequestMessage. For instance, I'm using client.GetStreamAsync, which none of its overloads take HttpRequestMessage.

In this case, I wonder how you would pass the Ilogger<T> to Polly's retry delegates.

Upvotes: 1

Views: 1379

Answers (1)

Peter Csala
Peter Csala

Reputation: 22829

Options that does not work for your use case

Using the Context object with HttpRequestMessage

As you have stated in your question this is not applicable, since you don't have a HttpRequestMessage instance on which you could access the Context via the request.GetPolicyExecutionContext call.

Using AddPolicyHandler + IServiceProvider

The AddPolicyHandler has an overload which provides access to the IServiceProvider and to the HttpRequestMessage. You could obtain an ILoggerFactory via provider.GetRequiredService<ILoggerFactory>() and then you could call factory.CreateLogger<T>.

The problem with this approach is that you don't know T at policy registration time, since you want to use the Controller as T.

Options that could work for your use case

Defining the policy inside your Controller

If you would define the policy inside the same class where you have the intention to use it then you could access the ILogger<YourController>.

There are two drawbacks of this approach:

  • You have to define (more or less) the same policy in every place where you want to use it
  • You have to explicitly call the ExecuteAsync

The first issue can be addressed via the PolicyRegistry

Registering the policy into PolicyRegistry and using Context

You can register your policy/ies into a PolicyRegistry and then you can obtain them (via IReadOnlyPolicyRegistry) inside your controller. This approach lets you define your policy in the way that you can retrieve an ILogger from the Context inside the onRetry. And you can specify the Context when you call the ExecuteAsync

var context = new Polly.Context().WithLogger(yourControllerLogger);
await policy.ExecuteAsync(async (ct)  => ..., context);

Registering the policy into PolicyRegistry and using try-catch

The previous approach used the Context to transfer an object between the policy definition and its usage. One can say that this separation is a bit fragile since the coupling between these two is not explicit rather via a magic Context object.

An alternative solution could be to perform logging only inside your the ExecuteAsync to avoid the usage of the Context

await policy.ExecuteAsync(async ()  => 
   try
   {
       ...
   }
   catch(Exception ex) //filter for the retry's trigger exception(s)
   {
       yourControllerLogger.LogError(...);
   });

As you can see none of the above solutions is perfect since you want to couple the policy and its usage via logging.


UPDATE #1

I'm not a big fan of defining policy inside a controller, because I generally reuse a policy (and accordingly the HttpClientFactory) in different controllers.

As I said above, this is one option out of three. The other two options do not require you to define your policy inside the controller class. You can define them inside the startup

var registry = new PolicyRegistry()
{
    { "YourPolicyName", resilientStrategy }
};
services.AddPolicyRegistry(registry);

and then retrieve the given policy inside the controller

private readonly IAsyncPolicy policy;
public YourController(IReadOnlyPolicyRegistry<string> registry)
{
    policy = registry.Get<IAsyncPolicy>("YourPolicyName"):
}

I suppose there is no other cleaner solution

If you want to / need to use the controller's logger inside the onRetry delegate then I'm unaware of any cleaner solution.

If you want to use that logger to be able to correlate the controller's log with the policy's log then I would rather suggest to use a correlation id per request and include that into your logs. Steve Gordon has a nuget package called correlationId which can help you to achieve that.

Upvotes: 2

Related Questions