0

I've been trying to follow the directions from this blog post to pass an ILogger to my retry policy in order to log information about the errors being retried.

The code in the blog doesn't work out of the box as we're using Refit for client generation. Based on the refit docs it should just be a matter of adding a property to my method signatures, but haven't been able to get it to actually work.

Even though I've added the property to my method signature:

    Task<UserSubscriptions> GetUserSubscriptions(string userId, [Property("PollyExecutionContext")] Polly.Context context);

I've captured logger management in extension methods:

    private static readonly string LoggerKey = "LoggerKey";

    public static Context WithLogger(this Context context, ILogger logger)
    {
        context[LoggerKey] = logger;
        return context;
    }

    public static ILogger GetLogger(this Context context)
    {
       if (context.TryGetValue(LoggerKey, out object logger))
       {
           return logger as ILogger;
       }
       return null;

    }

I create a new context when executing the method:

    public Context GetPollyContext() => new Context().WithLogger(logger);

    public Task<UserSubscriptions> GetUserSubscriptions(UserId userId) {
        return restClient.GetUserSubscriptions(userId.UserIdString, GetPollyContext());
    }

And try to access the logger as part of the retry action:

    return Policy
        .Handle<Exception>()
        .OrResult<HttpResponseMessage>(r => CodesToRetry.Contains(r.StatusCode))
        .WaitAndRetryAsync(3, retryCount => TimeSpan.FromSeconds(1), (result, timeSpan, retryCount, context) =>
        {
            var logger = context.GetLogger();
            if (logger == null) return;

            // do some logging
        }
    });

When I set a break point in the retry action the context that I see is a new empty context and not the one I created with the attached logger.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Bob Kinney
  • 8,870
  • 1
  • 27
  • 35
  • Have you tried to pass the logger [like this](https://github.com/App-vNext/Polly/wiki/Keys-and-Context-Data#example-varying-the-ilogger-used-within-an-onretry-delegate)? Rather than explicitly populating the contextData you can create some helper extension methods to do that for you. – Peter Csala May 25 '22 at 07:36
  • Thanks for the suggestion. Not sure this will make a difference, but I'll give it a try. – Bob Kinney May 25 '22 at 14:18
  • I'm not sure either :) Please let me know. I'm unfamiliar with refit so if you could put together a minimal, reproducible example which I could run locally then I could help better. – Peter Csala May 25 '22 at 15:54
  • 1
    It didn't help, but got a response from one of the refit maintainers and added an answer that addresses the issue. TL;DR - there was a typo in the docs, but there's a better way to do it anyway. – Bob Kinney May 25 '22 at 20:35

1 Answers1

1

Per GitHub issues, there was a typo, the property is PolicyExecutionContext, not PollyExecutionContext.

Though given I don't need to generate a unique context per request, the better pattern is to use delegate injection.

Extension methods

    private static readonly string LoggerKey = "LoggerKey";

    public static Context WithLogger(this Context context, ILogger logger)
    {
        context[LoggerKey] = logger;
        return context;
    }

    public static ILogger GetLogger(this Context context)
    {
       if (context.TryGetValue(LoggerKey, out object logger))
       {
           return logger as ILogger;
       }
       return null;

    }

Delegate definition

    public class PollyContextInjectingDelegatingHandler<T> : DelegatingHandler
    {
        private readonly ILogger<T> _logger;

        public PollyContextInjectingDelegatingHandler(ILogger<T> logger)
        {
            _logger = logger;
        }

        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            var pollyContext = new Context().WithLogger(_logger);
            request.SetPolicyExecutionContext(pollyContext);

            return await base.SendAsync(request,    cancellationToken).ConfigureAwait(false);
        }
    }

Then add the delegate to the client definition

    services
        .AddTransient<ISubscriptionApi, SubscriptionApi>()
        .AddTransient<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
        .AddRefitClient<ISubscriptionApiRest>(EightClientFactory.GetRefitSettings())
        .ConfigureHttpClient((s, c) =>
        {
            ...
        })
       .AddHttpMessageHandler<PollyContextInjectingDelegatingHandler<SubscriptionApi>>()
       .ApplyTransientRetryPolicy(retryCount, timeout);
Bob Kinney
  • 8,870
  • 1
  • 27
  • 35
  • Please be aware that ordering of `AddHttpMessageHandler` and `AddPolicyHandler` [does matter](https://stackoverflow.com/a/63700841/13268855). So, in your case you might need to change the last to extension method calls. – Peter Csala May 26 '22 at 06:57
  • 1
    Thanks, this was noted in the GH issue and I tried to maintain ordering of the last 2 methods, adding the message handler before applying the policy. In testing this ordering does produce the desired result. – Bob Kinney May 26 '22 at 16:16