ASP.NET Core中使用Graylog記錄日誌

以下基於.NET Core 2.1

定義GrayLog日誌記錄中間件:

中間件代碼:

 public class GrayLogMiddleware
 {
     private readonly RequestDelegate _next;
     private readonly ILogger _logger;

     //在應用程序的生命週期中,中間件的構造函數只會被調用一次
     public GrayLogMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
     {
         _next = next;
         _logger = loggerFactory.CreateLogger("GrayLog");
     }

     public async Task InvokeAsync(HttpContext context)
     {
         var additionalFields = new Dictionary<string, object>()
         {
             ["LogId"] = Guid.NewGuid()
         };

        // 若將該中間件做爲第一個請求管道中的第一個中間件進行註冊
        // 那麼在此處就可以進行全局異常處理
         try
         {
             var startTime = DateTime.Now;
             await _next(context);
             var endTime = DateTime.Now;
             additionalFields["Elapsed"] = (endTime - startTime).Milliseconds;
             _logger.LogInfo(context, additionalFields);
         }
         catch (Exception ex)
         {
             if (context.Response.HasStarted == false)
             {
                 await WriteExceptionInfoIntoResponseAsync(context, ex);
             }
             _logger.LogException(context, ex, additionalFields);
     #if DEBUG
             throw;
     #endif
         }
     }

     private async Task WriteExceptionInfoIntoResponseAsync(HttpContext context, Exception ex)
     {
         try
         {
             var resp = new ApiResponse();
             resp = resp.Exception(ex);
             var respStr = JsonConvert.SerializeObject(resp);
             await context.Response.WriteAsync(respStr, Encoding.UTF8);
         }
         catch
         {
             // ignore
         }
     }

 }

日誌記錄代碼:


public static class LogExtension
{
    public static void LogInfo(this ILogger logger, HttpContext context, IDictionary<string, object> addtionalFields = null)
    {
        logger.LogCore(context, LogLevel.Information, addtionalFields: addtionalFields);
    }

    public static void LogException(this ILogger logger, HttpContext context, Exception ex, IDictionary<string, object> addtionalFields = null)
    {
        logger.LogCore(context, LogLevel.Error, ex, addtionalFields);
    }

    private static void LogCore(this ILogger logger, HttpContext context, LogLevel logLevel, Exception ex = null, IDictionary<string, object> addtionalFields = null)
    {
        try
        {
            var shortMessage = GetShortMessage(context);
            if (addtionalFields == null)
            {
                addtionalFields = GetAddtionalFields(context);
            }
            else
            {
                var temp = GetAddtionalFields(context);
                addtionalFields = addtionalFields.Union(temp).ToDictionary(d => d.Key, d => d.Value);
            }

            // 需要使用Scope才能將additionalFields記錄到GrayLog中
            using (logger.BeginScope(addtionalFields))
            {
                logger.Log(logLevel, exception: ex, message: shortMessage);
            }
        }
        catch
        {
#if DEBUG
            throw;
#endif
            // ignore
        }
    }

    /// <summary>
    /// 獲取請求的短消息
    /// <para>
    /// 消息格式:HttpMethod RequestUrl HttpStatusCode
    /// </para>
    /// </summary>
    /// <example> GET http://localhost:5000 200</example>
    private static string GetShortMessage(HttpContext context)
    {
        var request = context.Request;

        var method = request.Method;
        var url = request.GetEncodedUrl();
        var statusCode = context.Response.StatusCode;

        return $"{method} {url} {statusCode}";
    }

    /// <summary>
    /// 需要寫入到日誌中的額外字段:請求來源,請求參數
    /// </summary>
    private static IDictionary<string, object> GetAddtionalFields(HttpContext context)
    {
        var referer = context.Connection.RemoteIpAddress;
        var requestData = GetRequestParameters(context);

        return new Dictionary<string, object>()
        {
            ["Referer"] = referer,
            ["RequestData"] = requestData
        };
    }

    private static string GetRequestParameters(HttpContext context)
    {
        if (context.Request.ContentLength > 0)
        {
            var stream = context.Request.Body;
            if (stream.CanRead == false)
            {
                return null;
            }
            if (stream.CanSeek == false)
            {
                // 將HttpRequestStream轉換爲FileBufferingReadStream
                context.Request.EnableBuffering();
                stream = context.Request.Body;
            }
            stream.Position = 0;

            using (var reader = new StreamReader(stream))
            {
                var data = reader.ReadToEnd();
                return data;
            }
        }

        return null;
    }

}

Graylog日誌配置:

    public class Program
    {
        public static void Main(string[] args) => CreateWebHost().Run();

        private static IWebHost CreateWebHost() => CreateWebHostBuilder().Build();

        // 這裏未使用.NET Core封裝的CreateDefaultBuilder方法,因爲它封裝了過多不需要的東西
        private static IWebHostBuilder CreateWebHostBuilder() =>
               new WebHostBuilder()
                            .UseContentRoot(Directory.GetCurrentDirectory())
#if RELEASE
            .UseIISIntegration()
#endif
                            .UseKestrel()
                            .ConfigureLogging((context, builder) =>
                            {
                                ConfigLogger(context, builder);
                            })
                            .UseStartup<Startup>();

        private static void ConfigLogger(WebHostBuilderContext context, ILoggingBuilder builder)
        {
            // 使用日誌過濾器(log filtering),禁止Kestrel記錄訪問日誌
            builder.ClearProviders();
            builder.AddFilter("Microsoft", LogLevel.None);
            builder.AddFilter("System", LogLevel.Error);

            if (context.HostingEnvironment.IsDevelopment())
            {
                builder.AddDebug();
            }

            // GrayLog配置(這裏使用App.config作爲配置文件
            builder.AddGelf(option =>
            {
                option.Host = ConfigurationManager.AppSettings["grayLogHost"];
                option.Port = Convert.ToInt32(ConfigurationManager.AppSettings["grayLogPort"]);
                option.LogSource = ConfigurationManager.AppSettings["grayLogSource"];
                option.Protocol = GelfProtocol.Udp;
            });
        }
    }

註冊中間件到請求處理管道:

public static class GrayLogMiddlewareExtension
{
    /// <summary>
    /// 向請求管道中添加GrayLog記錄功能及全局異常處理
    /// </summary>
    public static IApplicationBuilder UseGrayLog(this IApplicationBuilder builder) =>
        builder.UseMiddleware<GrayLogMiddleware>();
}

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseGrayLog()
            .UseMvc();
    }
}

以上日誌記錄瞭如下幾個方面:

  1. 日誌信息Id

  2. 請求來源

  3. 請求基礎信息

    採用類似HTTP請求行格式,即:HttpMethod RequestUrl ResponseStatusCode,如:GET http://localhost 200

  4. 入參

  5. 接口耗時

  6. 若發生異常,則記錄異常信息

HttpRequestStream vs FileBufferingReadStream

GET請求參數都體現在Url中了,這裏講述如何獲取POST請求的參數。

通常POST請求數據都在請求體中,ASP.NET Core中HttpRequest類型的Body屬性是HttpRequestStream類型,該類型源碼在Github上可以看到,但在Google和微軟關方文檔中都沒搜索到。反編譯Microsoft.AspNetCore.Server.Kestrel.Core.dll只找到了同樣繼承自ReadOnlyStreamFrameRequestStream

HttpRequestStream類的CanSeek屬性返回值爲false,不支持多次讀取,所以需要先轉換爲FileBufferingReadStream。轉換過程可參考:BufferingHelperHttpRequestRewindExtensions。實現代碼如下:

public static class HttpRequestRewindExtensions
{
    public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit)
    {
        BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit);
    }
}


public static class BufferingHelper
{
    public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null)
    {
        if (request == null)
        {
            throw new ArgumentNullException(nameof(request));
        }

        var body = request.Body;
        if (!body.CanSeek)
        {
            var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, _getTempDirectory);
            request.Body = fileStream;
            request.HttpContext.Response.RegisterForDispose(fileStream);
        }
        return request;
    }
}

推薦閱讀

Logging in ASP.NET Core

ASP.NET Core Middleware

Stream Class

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章