如何在.NET Core WebAPI中自动记录每个请求日志?

30

我希望自动记录每个请求。在以前的.Net Framework WebAPI项目中,我通常会注册一个DelegateHandler来完成此操作。

WebApiConfig.cs

public static void Register(HttpConfiguration config)
{
    config.MessageHandlers.Add(new AutoLogDelegateHandler());
}

AutoLogDelegateHandler.cs

->

自动记录委托处理程序.cs

public class AutoLogDelegateHandler : DelegatingHandler
{

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var requestBody = request.Content.ReadAsStringAsync().Result;

        return await base.SendAsync(request, cancellationToken)
            .ContinueWith(task =>
            {
                HttpResponseMessage response = task.Result;

                //Log use log4net
                _LogHandle(request, requestBody, response);

                return response;
            });
    }
}

日志内容的示例:

------------------------------------------------------
2017-08-02 19:34:58,840
uri: /emp/register
body: {
    "timeStamp": 1481013427,
    "id": "0322654451",
    "type": "t3",
    "remark": "system auto reg"
}
response: {"msg":"c556f652fc52f94af081a130dc627433","success":"true"}
------------------------------------------------------

然而在.NET Core WebAPI项目中,没有WebApiConfig或者在Global.asax中注册函数的GlobalConfiguration.Configure(WebApiConfig.Register);

那么,在.NET Core WebAPI中有什么方法可以实现这个功能吗?

6个回答

47

ActionFilter 将一直起作用,直到您需要仅记录由 MVC 中间件(例如控制器操作)处理的请求。

如果您需要记录所有传入请求,则需要使用中间件方法。

好的视觉解释enter image description here

请注意,中间件顺序很重要,如果您的日志记录应在管道执行开始时完成,则您的中间件应该是最前面的之一。

来自文档的简单示例:

public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do loging
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

这个能在我的API运行在AWS API Gateway上工作吗?这是我的问题,我需要一些帮助... https://stackoverflow.com/questions/59167427/httpcontext-request-is-blank-for-a-middleware-running-on-aws-api-gateway - ATHER
我认为你的意思是ActionFilter只能用于记录MVC中间件处理的请求? - Professor of programming

15

对于想要快速且(非常)简单的调试解决方案(适用于 .Net Core 3)的人来说,这里是this answer的扩展,这就是你所需要的...

app.Use(async (context, next) =>
{
    var initialBody = context.Request.Body;

    using (var bodyReader = new StreamReader(context.Request.Body))
    {
        string body = await bodyReader.ReadToEndAsync();
        Console.WriteLine(body);
        context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
        await next.Invoke();
        context.Request.Body = initialBody;
    }
});

谢谢。你能解释一下这里对主体内容进行操作的原因吗? - GGleGrand
5
@GGleGrand 好久不见了,但我猜是因为代码试图读取两次流——一次用于在这里进行记录,一次是“真正的”读取。因此,在记录完后,我需要重置流。 - ihake

13

你可以创建自己的筛选器属性...

public class InterceptionAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(HttpActionContext actionContext)
  {
    var x = "This is my custom line of code I need executed before any of the controller actions, for example log stuff";
    base.OnActionExecuting(actionContext);
  }
}

......并且您需要使用GlobalFilters进行注册,但由于您说您正在使用.NET Core,因此您可以尝试按照以下方式进行......

来自learn.microsoft.com

您可以通过将其添加到Startup类中的ConfigureServices方法中的MvcOptions.Filters集合中,全局注册过滤器(适用于所有控制器和操作):

如果成功,请通知我们。

P.S. 这里有一个关于如何使用WebAPI拦截所有传入请求的完整教程,以防有人需要更多细节。


5
是的,它有效。我创建了一个 AutoLogAttribute,并在 OnActionExecuted 中记录了每个请求。然后通过将其添加到 Startup 类中的 ConfigureServices 方法中的 MvcOptions.Filters 集合中全局注册过滤器。 - wtf512
@Eedoh 你是怎么做到的? - Sana Ahmed

7

示例:

AutologArribute.cs(新文件)

/// <summary>
/// <see cref="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#Dependency injection"/>
/// </summary>
public class AutoLogAttribute : TypeFilterAttribute
    {
        public AutoLogAttribute() : base(typeof(AutoLogActionFilterImpl))
        {

        }

        private class AutoLogActionFilterImpl : IActionFilter
        {
            private readonly ILogger _logger;
            public AutoLogActionFilterImpl(ILoggerFactory loggerFactory)
            {
                _logger = loggerFactory.CreateLogger<AutoLogAttribute>();
            }

            public void OnActionExecuting(ActionExecutingContext context)
            {
                // perform some business logic work
            }

            public void OnActionExecuted(ActionExecutedContext context)
            {
                //TODO: log body content and response as well
                _logger.LogDebug($"path: {context.HttpContext.Request.Path}"); 
            }
        }
    }

StartUp.cs

public void ConfigureServices(IServiceCollection services)
{
    //....

    // https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#filter-scopes-and-order-of-execution
    services.AddMvc(opts=> {
        opts.Filters.Add(new AutoLogAttribute());
    });

    //....
}

6
这是一个适用于.NET Core 2.2 Web API的完整日志组件。 它将记录请求和响应,包括头部和正文。 只需确保您有一个名为“Logs”的文件夹即可。
AutoLogMiddleWare.cs(新文件)
public class AutoLogMiddleWare
{
    private readonly RequestDelegate _next;

    public AutoLogMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            string route = context.Request.Path.Value;
            string httpStatus = "0";

            // Log Request
            var originalRequestBody = context.Request.Body;
            originalRequestBody.Seek(0, SeekOrigin.Begin);
            string requestBody = new StreamReader(originalRequestBody).ReadToEnd();
            originalRequestBody.Seek(0, SeekOrigin.Begin);

            // Log Response
            string responseBody = string.Empty;
            using (var swapStream = new MemoryStream())
            {

                var originalResponseBody = context.Response.Body;
                context.Response.Body = swapStream;
                await _next(context);
                swapStream.Seek(0, SeekOrigin.Begin);
                responseBody = new StreamReader(swapStream).ReadToEnd();
                swapStream.Seek(0, SeekOrigin.Begin);
                await swapStream.CopyToAsync(originalResponseBody);
                context.Response.Body = originalResponseBody;
                httpStatus = context.Response.StatusCode.ToString();
            }

            // Clean route
            string cleanRoute = route;
            foreach (var c in Path.GetInvalidFileNameChars())
            {
                cleanRoute = cleanRoute.Replace(c, '-');
            }

            StringBuilder sbRequestHeaders = new StringBuilder();
            foreach (var item in context.Request.Headers)
            {
                sbRequestHeaders.AppendLine(item.Key + ": " + item.Value.ToString());
            }

            StringBuilder sbResponseHeaders = new StringBuilder();
            foreach (var item in context.Response.Headers)
            {
                sbResponseHeaders.AppendLine(item.Key + ": " + item.Value.ToString());
            }


            string filename = DateTime.Now.ToString("yyyyMMdd.HHmmss.fff") + "_" + httpStatus + "_" + cleanRoute + ".log";
            StringBuilder sbLog = new StringBuilder();
            sbLog.AppendLine("Status: " + httpStatus + " - Route: " + route);
            sbLog.AppendLine("=============");
            sbLog.AppendLine("Request Headers:");
            sbLog.AppendLine(sbRequestHeaders.ToString());
            sbLog.AppendLine("=============");
            sbLog.AppendLine("Request Body:");
            sbLog.AppendLine(requestBody);
            sbLog.AppendLine("=============");
            sbLog.AppendLine("Response Headers:");
            sbLog.AppendLine(sbResponseHeaders.ToString());
            sbLog.AppendLine("=============");
            sbLog.AppendLine("Response Body:");
            sbLog.AppendLine(responseBody);
            sbLog.AppendLine("=============");

            var path = Directory.GetCurrentDirectory();
            string filepath = ($"{path}\\Logs\\{filename}");
            File.WriteAllText(filepath, sbLog.ToString());
        }
        catch (Exception ex)
        {
            // It cannot cause errors no matter what
        }

    }
}

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

Startup.cs (现有文件)

public void Configure(IApplicationBuilder app, IHostingEnvironment env, IMapper mapper, ILoggerFactory loggerFactory)
{
    bool isLogEnabled = true; // Replace it by some setting, Idk

    if(isLogEnabled)
        app.UseEnableRequestRewind(); // Add this first

    (...)

    if(isLogEnabled)
        app.UseMiddleware<AutoLogMiddleWare>(); // Add this just above UseMvc()

    app.UseMvc();
}

4

从ASP.NET Core 6开始,您可以使用默认中间件来实现此行为(来源):

app.UseHttpLogging();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接