Bob Kinney
Bob Kinney

Reputation: 9030

Using an ILogger in a Polly Policy attached to a Refit Client

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.

Upvotes: 0

Views: 928

Answers (1)

Bob Kinney
Bob Kinney

Reputation: 9030

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);

Upvotes: 1

Related Questions