grpc/grpc-dotnet

How to pass Serilog correlationId from HTTP request header to grpc call

shashankvivek opened this issue · 2 comments

I want to have a correlationId that can be used to track calls across different micro services.

Here is what I am aiming for:

  1. UI client calling BE (MicroService-A) with correlation-id.
  2. MicroService-A uses this correlation-id and adds it to the Header of Grpc Metadata to call MicroService-B. If not there, serilog generates it automatically using Serilog enricher client-info

.Enrich.WithCorrelationId(headerName: "correlation-id", addValueIfHeaderAbsence: true)

  1. MicroService-B add this to serilog using LogContext.

I have been able to achieve point 1 and 3. For point 2, I am not sure how to get the correlation-id from HttpContext into my Grpc Interceptor.

Here is what I have done.

Client interceptor (MicroService-A):

public class GrpcClientLoggerInterceptor : Interceptor
{
   public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var metadata = new Metadata();
        metadata
          .Add("x-correlation-id", "http-request-id");  // <-- This is where I need to get from 
                    //current HTTP context and set into Grpc metadata.

        var newContext = new ClientInterceptorContext<TRequest, TResponse>(
            context.Method,
            context.Host,
            context.Options.WithHeaders(
                (context.Options.Headers ?? new Metadata()).Aggregate(
                    metadata,
                    AddIfNonExistent))
        );

        return continuation(request, newContext);
    }

    private static Metadata AddIfNonExistent(Metadata metadata, Metadata.Entry entry)
    {
        if (metadata.Get(entry.Key) == null) metadata.Add(entry);
        return metadata;
    }
}

Server interceptor (MicroService-B)

public class GrpcServerLoggerInterceptorBase : Interceptor
{
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
        {
                var correlationIdHeader = context.RequestHeaders.FirstOrDefault(m => String.Equals(m.Key, "x-correlation-id", StringComparison.Ordinal));
                var correlationId = correlationIdHeader?.Value == null ? new Guid().ToString() : correlationIdHeader?.Value;
                
                using (LogContext.PushProperty("x-correlation-id", correlationId))
                {
                    return await base.UnaryServerHandler(request, context, continuation);
                }
        }
}

What is the best way to access correlation-id from serilog and get the "x-correlation-id" inside my Client Grpc interceptor? How can I get HttpContext here

An async local is easiest. Either define your own, or use Activity, or use IHttpContextAccessor.

https://github.com/grpc/grpc-dotnet/blob/master/src/Grpc.AspNetCore.Server.ClientFactory/ContextPropagationInterceptor.cs is an example of using IHttpContextAccessor. You would get and set headers rather than the cancellation token.

Thanks @JamesNK .

For others, I made below changes:

  1. I used InterceptorRegistrations in place of Interceptor because it is now deprecated. This gave me access to IServiceProvider
 opts.InterceptorRegistrations.Add( new InterceptorRegistration(
                    InterceptorScope.Client,
                    s =>
                    {
                        var accessor = s.GetRequiredService<IHttpContextAccessor>();
                        return new GrpcClientLoggerInterceptor(accessor);
                    }));
  1. Then I injected IHttpContextAccessor in my interceptor:
    private readonly IHttpContextAccessor httpContext;
    public GrpcClientLoggerInterceptor(IHttpContextAccessor httpContext)
    {
        this.httpContext = httpContext;
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var correlationId = httpContext?.HttpContext?.Request.Headers["x-correlation-id"] ?? new Guid().ToString();
        var metadata = new Metadata
        {
            { "x-correlation-id", correlationId}
        };

        var newContext = new ClientInterceptorContext<TRequest, TResponse>(
            context.Method,
            context.Host,
            context.Options.WithHeaders(
                (context.Options.Headers ?? new Metadata()).Aggregate(
                    metadata,
                    AddIfNonExistent))
        );


        return continuation(request, newContext);
    }

    private static Metadata AddIfNonExistent(Metadata metadata, Metadata.Entry entry)
    {
        if (metadata.Get(entry.Key) == null) metadata.Add(entry);
        return metadata;
    }