以下基於.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();
}
}
以上日誌記錄瞭如下幾個方面:
日誌信息Id
請求來源
-
請求基礎信息
採用類似HTTP請求行格式,即:
HttpMethod RequestUrl ResponseStatusCode
,如:GET http://localhost 200
入參
接口耗時
若發生異常,則記錄異常信息
HttpRequestStream vs FileBufferingReadStream
GET請求參數都體現在Url中了,這裏講述如何獲取POST請求的參數。
通常POST請求數據都在請求體中,ASP.NET Core中HttpRequest類型的Body屬性是HttpRequestStream類型,該類型源碼在Github上可以看到,但在Google和微軟關方文檔中都沒搜索到。反編譯Microsoft.AspNetCore.Server.Kestrel.Core.dll
只找到了同樣繼承自ReadOnlyStream的FrameRequestStream
HttpRequestStream類的CanSeek屬性返回值爲false,不支持多次讀取,所以需要先轉換爲FileBufferingReadStream。轉換過程可參考:BufferingHelper及HttpRequestRewindExtensions。實現代碼如下:
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;
}
}