Home > database >  .net core 7 minimal post body logging
.net core 7 minimal post body logging

Time:01-07

I use .net core 7, minimal api and I would like to log POST request body.

I use following code:

        var endpoints = app.MapGroup("api/v1")
            .AddEndpointFilter<RequestResponseLogFilter>();

        endpoints.MapPost("/endpoint", async ([FromServices] ServiceA serviceA, [FromServices] ServiceB serviceB, [FromBody] MyRequest request) =>
            await MyEndpoint.DoSomething(serviceA, serviceB, request));

Attribute that I use to distinguish that object is request body:

public class RequestBodyAttribute : Attribute
{
}

[RequestBody]
public class MyRequest
{
}

Logging filter:

public class RequestResponseLogFilter : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        var httpContext = context.HttpContext;
        var logger = httpContext.RequestServices.GetService<ILogger>()!;

        object? requestBody = context.Arguments.FirstOrDefault(a => a?.GetType().GetCustomAttribute<RequestBodyAttribute>() != null);

        logger.Information("{@Request}", LogHelper.PrepareRequest(httpContext, requestBody));

        var stopwatch = Stopwatch.StartNew();
        var result = await next(context);
        stopwatch.Stop();

        logger.Information("{@Response}", LogHelper.PrepareResponse(httpContext, result, stopwatch.Elapsed));
        return result;
    }

I do not like approach that I need to put [RequestBody] attribute on each class that I use as POST body - this could lead to errors, I can forget to put attribute.

Another approach would be to use attribute position (for example last attribute is body), but this also could lead to errors.

I thinks, the best way would be to check for attribute [FromBody], but I do not have this info in my filter.

What would be the best way to distinguish between services and body in my filter (assuming that I use generic filter to process any DTO)?

CodePudding user response:

Not a direct answer to the question, but first of all note that Minimal APIs support HttpLogging and W3C logging, check it out, maybe it will be enough:

builder.Services
    .AddHttpLogging(logging =>
    {
        logging.LoggingFields = HttpLoggingFields.All;
        logging.RequestBodyLogLimit = 4096;
        logging.ResponseBodyLogLimit = 4096;
    });

// ...
var app = builder.Build();
app.UseHttpLogging();
// ...

And in settings:

{
  "Logging": {
    "LogLevel": {
      // ...
      "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
    }
  }
}

Though there is no "easy" way to enable it only for concrete endpoints, but you can workaround with UseWhen - instead of app.UseHttpLogging();:

app.UseWhen(ctx => ctx.Request.Path.Value?.Contains("test") == true, // some predicate
    appBuilder => appBuilder.UseHttpLogging());

If this still is not suitable for use case I would argue still relying on handler parameters having attributes is still not a best option (FromBodyAttribute is actually not required and can be skipped). You probably should consider approach used in HttpLoggingMiddleware itself - by reading the request stream and logging the results (see the source code).

CodePudding user response:

I've managed to find solution, it could be solved by Filter Factory:

var endpoints = app.MapGroup("api/v1")
            .AddEndpointFilterFactory(RequestResponseLogFilterFactory.AddLoggingFilter);

        endpoints.MapPost("/endpoint", async ([FromServices] ServiceA serviceA, [FromServices] ServiceB serviceB, [FromBody] MyRequest request) =>
            await MyEndpoint.DoSomething(serviceA, serviceB, request));

Factory itself:

public static class RequestResponseLogFilterFactory
{
    public static EndpointFilterDelegate AddLoggingFilter(EndpointFilterFactoryContext context, EndpointFilterDelegate next)
    {
        var parameters = context.MethodInfo.GetParameters();
        var parameter = parameters.FirstOrDefault(p => 
            p.CustomAttributes.Any(a => a.AttributeType == typeof(Microsoft.AspNetCore.Mvc.FromBodyAttribute)));

        return async invocationContext =>
        {
            var httpContext = invocationContext.HttpContext;
            var logger = httpContext.RequestServices.GetService<ILogger>()!;

            object? requestBody = null;

            if(parameter is not null)
            {
                requestBody = invocationContext.Arguments.FirstOrDefault(a => a?.GetType() == parameter.ParameterType);
            }

            logger.Information("{@Request}", LogHelper.PrepareRequest(httpContext, requestBody));

            var stopwatch = Stopwatch.StartNew();
            var result = await next(invocationContext);
            stopwatch.Stop();

            logger.Information("{@Response}", LogHelper.PrepareResponse(httpContext, result, stopwatch.Elapsed));
            return result;
        };
    }
  • Related