以下基于.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;
}
}

随时随地看视频