我们正在尝试通过 记录大型数据对象ILogger.Log
,但正在修剪我们发送到 ApplicationInsights 的数据。我们尝试记录的数据对象是来自集成源的请求/响应,用于在开发期间进行诊断。不用说,请求/响应会变得非常大。
我们的第一次尝试是从请求/响应中读取内容作为字符串,然后通过ILogger.Log
. 但是,我们很快意识到内容被修剪了。经过进一步调查,我们发现 ApplicationInsights 属性值字符串长度限制为 8192 字节 ( https://github.com/MicrosoftDocs/azure-docs/blob/master/includes/application-insights-limits.md )。
我们的下一个尝试是反序列化请求/响应并将其作为对象 (JToken) 通过ILogger.Log
. 我们希望 ApplicationInsightsLoggingProvider 能够以更结构化的方式记录对象,从而完全跳过 8192 字节的属性值字符串长度限制。然而,不幸的是,这让事情变得更糟。请求/响应仍以字符串形式记录在 ApplicationInsights 中,但现在采用每个字符占用两个字节而不是一个字节的格式。基本上将我们的日志记录能力减半。
附加代码是我们当前的实现。我们正在通过DelegatingHandler
. 它HttpClient
在 ASP 之后LogicalHandler
通过自定义插入到传出管道中IHttpMessageHandlerBuilderFilter
。我们的DelegatingHandler
使用LoggerMessage
模式 ( https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/loggermessage?view=aspnetcore-3.0 ),并且仅在日志级别设置为跟踪时才会记录。当前的实现是第二版,我们尝试将请求/响应提取的 JSON 字符串反序列化为 JToken(第 107 行)并通过ILogger.Log
. 如果反序列化失败,它将回退到我们的版本一等效实现,原始字符串将被发送到ILogger.Log
.
我们正在使用来自Microsoft.ApplicationInsights.AspNetCore
version的 ApplicationInsightsLoggingProvider 2.8.2
。
所以问题就变成了:我们如何通过 ILogger 将大型对象记录到 ApplicationInsights,最好是结构化格式,以便我们可以使用 kusto 对其进行查询?
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Shared.Extensions.IntegrationPolicies
{
public class CustomLoggingFilter : IHttpMessageHandlerBuilderFilter
{
private readonly ILoggerFactory _loggerFactory;
public CustomLoggingFilter(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
return builder =>
{
next(builder);
var logger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{builder.Name}.CustomLogger");
builder.AdditionalHandlers.Insert(1, new CustomLoggingHandler(logger));
};
}
}
internal class CustomLoggingHandler : DelegatingHandler
{
private readonly ILogger _logger;
public CustomLoggingHandler(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
await Log.LogRequest(_logger, request);
var response = await base.SendAsync(request, cancellationToken);
await Log.LogResponse(_logger, response);
return response;
}
private static class Log
{
private static readonly LogLevel LogLevel = LogLevel.Trace;
private static readonly Action<ILogger, HttpMethod, Uri, object, Exception> LogRequestAction =
LoggerMessage.Define<HttpMethod, Uri, object>(
LogLevel,
EventIds.PipelineStart,
$"Start processing HTTP request {{HttpMethod}} {{Uri}}{Environment.NewLine}{{Body}}");
private static readonly Action<ILogger, HttpStatusCode, object, Exception> LogResponseAction =
LoggerMessage.Define<HttpStatusCode, object>(
LogLevel,
EventIds.PipelineEnd,
$"End processing HTTP request - {{StatusCode}}{Environment.NewLine}{{Body}}");
public static async Task LogRequest(ILogger logger, HttpRequestMessage request)
{
if (!logger.IsEnabled(LogLevel))
{
return;
}
var content = await GetContent(request.Content);
LogRequestAction(logger, request.Method, request.RequestUri, content, null);
}
public static async Task LogResponse(ILogger logger, HttpResponseMessage response)
{
if (!logger.IsEnabled(LogLevel))
{
return;
}
var content = await GetContent(response.Content);
LogResponseAction(logger, response.StatusCode, content, null);
}
private static async Task<object> GetContent(HttpContent content)
{
if (content == null)
{
return null;
}
await content.LoadIntoBufferAsync();
var contentAsString = await content.ReadAsStringAsync();
return TryParseJson<object>(contentAsString, out var contentAsObject)
? contentAsObject
: contentAsString;
}
private static bool TryParseJson<T>(string value, out T result)
{
var success = true;
var settings = new JsonSerializerSettings
{
Error = (sender, args) =>
{
success = false;
args.ErrorContext.Handled = true;
}
};
result = JsonConvert.DeserializeObject<T>(value, settings);
return success;
}
private static class EventIds
{
public static readonly EventId PipelineStart = new EventId(100, "RequestPipelineStart");
public static readonly EventId PipelineEnd = new EventId(101, "RequestPipelineEnd");
}
}
}
}