Просмотреть тело запроса POST в Application Insights
10 ответов
Вы можете просто реализовать свой собственный инициализатор телеметрии:
Например, ниже реализации, которая извлекает полезную нагрузку и добавляет ее в качестве пользовательского измерения телеметрии запроса:
public class RequestBodyInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry != null && (requestTelemetry.HttpMethod == HttpMethod.Post.ToString() || requestTelemetry.HttpMethod == HttpMethod.Put.ToString()))
{
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
string requestBody = reader.ReadToEnd();
requestTelemetry.Properties.Add("body", requestBody);
}
}
}
}
Затем добавьте его в конфигурацию либо с помощью файла конфигурации, либо с помощью кода:
TelemetryConfiguration.Active.TelemetryInitializers.Add(new RequestBodyInitializer());
Затем запросите его в Google Analytics:
requests | limit 1 | project customDimensions.body
Решение, предоставленное @yonisha, на мой взгляд, самое чистое из доступных. Тем не менее, вам все еще нужно получить свой httpcontext, и для этого вам понадобится еще немного кода. Я также вставил некоторые комментарии, которые основаны или взяты из примеров кода выше. Важно сбросить позицию вашего запроса, иначе вы потеряете его данные.
Это моё решение, которое я протестировал и которое дает мне jsonbody:
public class RequestBodyInitializer : ITelemetryInitializer
{
readonly IHttpContextAccessor httpContextAccessor;
public RequestBodyInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
if (telemetry is RequestTelemetry requestTelemetry)
{
if ((httpContextAccessor.HttpContext.Request.Method == HttpMethods.Post ||
httpContextAccessor.HttpContext.Request.Method == HttpMethods.Put) &&
httpContextAccessor.HttpContext.Request.Body.CanRead)
{
const string jsonBody = "JsonBody";
if (requestTelemetry.Properties.ContainsKey(jsonBody))
{
return;
}
//Allows re-usage of the stream
httpContextAccessor.HttpContext.Request.EnableRewind();
var stream = new StreamReader(httpContextAccessor.HttpContext.Request.Body);
var body = stream.ReadToEnd();
//Reset the stream so data is not lost
httpContextAccessor.HttpContext.Request.Body.Position = 0;
requestTelemetry.Properties.Add(jsonBody, body);
}
}
}
Тогда также обязательно добавьте это в свой Startup -> ConfigureServices
services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();
РЕДАКТИРОВАТЬ:
Если вы также хотите получить ответный орган, я считаю полезным создать один компонент промежуточного программного обеспечения (ядро dotnet не уверено в фреймворке). Сначала я придерживался вышеизложенного подхода, когда вы регистрируете ответ и запрос, но в большинстве случаев вы хотите получить их вместе.
public async Task Invoke(HttpContext context)
{
var reqBody = await this.GetRequestBodyForTelemetry(context.Request);
var respBody = await this.GetResponseBodyForTelemetry(context);
this.SendDataToTelemetryLog(reqBody, respBody, context);
}
Это ожидает и запрос, и ответ, где запрос в значительной степени такой же, как и выше, вместо этого, что это задача.
Для тела ответа я использовал приведенный ниже код, я также исключил 204, так как это приводит к нулю:
public async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
Stream originalBody = context.Response.Body;
try
{
using (var memStream = new MemoryStream())
{
context.Response.Body = memStream;
//await the responsebody
await next(context);
if (context.Response.StatusCode == 204)
{
return null;
}
memStream.Position = 0;
var responseBody = new StreamReader(memStream).ReadToEnd();
//make sure to reset the position so the actual body is still available for the client
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
return responseBody;
}
}
finally
{
context.Response.Body = originalBody;
}
}
Я выбираю собственный путь к промежуточному программному обеспечению, так как это упростило работу с
HttpContext
уже там.
public class RequestBodyLoggingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var method = context.Request.Method;
// Ensure the request body can be read multiple times
context.Request.EnableBuffering();
// Only if we are dealing with POST or PUT, GET and others shouldn't have a body
if (context.Request.Body.CanRead && (method == HttpMethods.Post || method == HttpMethods.Put))
{
// Leave stream open so next middleware can read it
using var reader = new StreamReader(
context.Request.Body,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: false,
bufferSize: 512, leaveOpen: true);
var requestBody = await reader.ReadToEndAsync();
// Reset stream position, so next middleware can read it
context.Request.Body.Position = 0;
// Write request body to App Insights
var requestTelemetry = context.Features.Get<RequestTelemetry>();
requestTelemetry?.Properties.Add("RequestBody", requestBody);
}
// Call next middleware in the pipeline
await next(context);
}
}
И вот как я регистрирую тело ответа
public class ResponseBodyLoggingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var originalBodyStream = context.Response.Body;
try
{
// Swap out stream with one that is buffered and suports seeking
using var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
// hand over to the next middleware and wait for the call to return
await next(context);
// Read response body from memory stream
memoryStream.Position = 0;
var reader = new StreamReader(memoryStream);
var responseBody = await reader.ReadToEndAsync();
// Copy body back to so its available to the user agent
memoryStream.Position = 0;
await memoryStream.CopyToAsync(originalBodyStream);
// Write response body to App Insights
var requestTelemetry = context.Features.Get<RequestTelemetry>();
requestTelemetry?.Properties.Add("ResponseBody", responseBody);
}
finally
{
context.Response.Body = originalBodyStream;
}
}
}
Затем добавьте метод расширения ...
public static class ApplicationInsightExtensions
{
public static IApplicationBuilder UseRequestBodyLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestBodyLoggingMiddleware>();
}
public static IApplicationBuilder UseResponseBodyLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseBodyLoggingMiddleware>();
}
}
... что обеспечивает чистую интеграцию внутри
Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// Enable our custom middleware
app.UseRequestBodyLogging();
app.UseResponseBodyLogging();
}
// ...
}
Не забудьте зарегистрировать пользовательские компоненты промежуточного программного обеспечения внутри
ConfigureServices()
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddApplicationInsightsTelemetry(Configuration["APPINSIGHTS_CONNECTIONSTRING"]);
services.AddTransient<RequestBodyLoggingMiddleware>();
services.AddTransient<ResponseBodyLoggingMiddleware>();
}
Вы можете прочитать об этом здесь.
Несколько дней назад у меня появилось аналогичное требование регистрировать основную часть запроса в приложениях с фильтрацией конфиденциальных входных данных пользователя из полезной нагрузки. Так что делюсь своим решением. Приведенное ниже решение разработано для веб-API ASP.NET Core 2.0.
ActionFilterAttribute
Я использовал ActionFilterAttribute
от (Microsoft.AspNetCore.Mvc.Filters
пространство имен), которая предоставляет модель через ActionArgument
так что при отражении можно извлечь те свойства, которые помечены как чувствительные.
public class LogActionFilterAttribute : ActionFilterAttribute
{
private readonly IHttpContextAccessor httpContextAccessor;
public LogActionFilterAttribute(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext.Request.Method == HttpMethods.Post || context.HttpContext.Request.Method == HttpMethods.Put)
{
// Check parameter those are marked for not to log.
var methodInfo = ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)context.ActionDescriptor).MethodInfo;
var noLogParameters = methodInfo.GetParameters().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(p => p.Name);
StringBuilder logBuilder = new StringBuilder();
foreach (var argument in context.ActionArguments.Where(a => !noLogParameters.Contains(a.Key)))
{
var serializedModel = JsonConvert.SerializeObject(argument.Value, new JsonSerializerSettings() { ContractResolver = new NoPIILogContractResolver() });
logBuilder.AppendLine($"key: {argument.Key}; value : {serializedModel}");
}
var telemetry = this.httpContextAccessor.HttpContext.Items["Telemetry"] as Microsoft.ApplicationInsights.DataContracts.RequestTelemetry;
if (telemetry != null)
{
telemetry.Context.GlobalProperties.Add("jsonBody", logBuilder.ToString());
}
}
await next();
}
}
LogActionFilterAttribute внедряется в конвейер MVC как фильтр.
services.AddMvc(options =>
{
options.Filters.Add<LogActionFilterAttribute>();
});
NoLogAttribute
В приведенном выше коде, NoLogAttribute
Атрибут используется, который должен быть применен к Свойствам Модели / Модели или параметру метода, чтобы указать, что значение не должно быть зарегистрировано.
public class NoLogAttribute : Attribute
{
}
NoPIILogContractResolver
Также, NoPIILogContractResolver
используется в JsonSerializerSettings
в процессе сериализации
internal class NoPIILogContractResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = new List<JsonProperty>();
if (!type.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute)))
{
IList<JsonProperty> retval = base.CreateProperties(type, memberSerialization);
var excludedProperties = type.GetProperties().Where(p => p.GetCustomAttributes(true).Any(t => t.GetType() == typeof(NoLogAttribute))).Select(s => s.Name);
foreach (var property in retval)
{
if (excludedProperties.Contains(property.PropertyName))
{
property.PropertyType = typeof(string);
property.ValueProvider = new PIIValueProvider("PII Data");
}
properties.Add(property);
}
}
return properties;
}
}
internal class PIIValueProvider : IValueProvider
{
private object defaultValue;
public PIIValueProvider(string defaultValue)
{
this.defaultValue = defaultValue;
}
public object GetValue(object target)
{
return this.defaultValue;
}
public void SetValue(object target, object value)
{
}
}
PIITelemetryInitializer
Внедрить RequestTelemetry
объект, я должен использовать ITelemetryInitializer
чтобы RequestTelemetry
можно найти в LogActionFilterAttribute
учебный класс.
public class PIITelemetryInitializer : ITelemetryInitializer
{
IHttpContextAccessor httpContextAccessor;
public PIITelemetryInitializer(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public void Initialize(ITelemetry telemetry)
{
if (this.httpContextAccessor.HttpContext != null)
{
if (telemetry is Microsoft.ApplicationInsights.DataContracts.RequestTelemetry)
{
this.httpContextAccessor.HttpContext.Items.TryAdd("Telemetry", telemetry);
}
}
}
}
PIITelemetryInitializer
зарегистрирован как
services.AddSingleton<ITelemetryInitializer, PIITelemetryInitializer>();
Функция тестирования
Следующий код демонстрирует использование вышеуказанного кода
Создан контроллер
[Route("api/[controller]")]
public class ValuesController : Controller
{
private readonly ILogger _logger;
public ValuesController(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<ValuesController>();
}
// POST api/values
[HttpPost]
public void Post([FromBody, NoLog]string value)
{
}
[HttpPost]
[Route("user")]
public void AddUser(string id, [FromBody]User user)
{
}
}
куда User
Модель определяется как
public class User
{
[NoLog]
public string Id { get; set; }
public string Name { get; set; }
public DateTime AnneviseryDate { get; set; }
[NoLog]
public int LinkId { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public string AddressLine { get; set; }
[NoLog]
public string City { get; set; }
[NoLog]
public string Country { get; set; }
}
Поэтому, когда API вызывается инструментом Swagger
JsonBody зарегистрирован в Запрос без конфиденциальных данных. Все конфиденциальные данные заменяются строковым литералом 'PII Data'.
Я так и не получил ответ @yonisha, поэтому я использовал DelegatingHandler
вместо:
public class MessageTracingHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Trace the request
await TraceRequest(request);
// Execute the request
var response = await base.SendAsync(request, cancellationToken);
// Trace the response
await TraceResponse(response);
return response;
}
private async Task TraceRequest(HttpRequestMessage request)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var requestTraceInfo = request.Content != null ? await request.Content.ReadAsByteArrayAsync() : null;
var body = requestTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Request Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
private async Task TraceResponse(HttpResponseMessage response)
{
try
{
var requestTelemetry = HttpContext.Current?.GetRequestTelemetry();
var responseTraceInfo = response.Content != null ? await response.Content.ReadAsByteArrayAsync() : null;
var body = responseTraceInfo.ToString();
if (!string.IsNullOrWhiteSpace(body) && requestTelemetry != null)
{
requestTelemetry.Properties.Add("Response Body", body);
}
}
catch (Exception exception)
{
// Log exception
}
}
}
.GetRequestTelemetry()
является методом расширения из https://www.nuget.org/packages/Microsoft.ApplicationInsights.Web/.
В ядре Asp.Net похоже, что нам не нужно использовать ITelemetryInitializer. Мы можем использовать промежуточное ПО для регистрации запросов к аналитическим данным приложения. Благодаря @IanKemp https://github.com/microsoft/ApplicationInsights-aspnetcore/issues/686
public async Task Invoke(HttpContext httpContext)
{
var requestTelemetry = httpContext.Features.Get<RequestTelemetry>();
//Handle Request
var request = httpContext.Request;
if (request?.Body?.CanRead == true)
{
request.EnableBuffering();
var bodySize = (int)(request.ContentLength ?? request.Body.Length);
if (bodySize > 0)
{
request.Body.Position = 0;
byte[] body;
using (var ms = new MemoryStream(bodySize))
{
await request.Body.CopyToAsync(ms);
body = ms.ToArray();
}
request.Body.Position = 0;
if (requestTelemetry != null)
{
var requestBodyString = Encoding.UTF8.GetString(body);
requestTelemetry.Properties.Add("RequestBody", requestBodyString);
}
}
}
await _next(httpContext); // calling next middleware
}
Я реализовал промежуточное программное обеспечение для этого,
Метод Invoke делает,
if (context.Request.Method == "POST" || context.Request.Method == "PUT")
{
var bodyStr = GetRequestBody(context);
var telemetryClient = new TelemetryClient();
var traceTelemetry = new TraceTelemetry
{
Message = bodyStr,
SeverityLevel = SeverityLevel.Verbose
};
//Send a trace message for display in Diagnostic Search.
telemetryClient.TrackTrace(traceTelemetry);
}
Где, как GetRequestBody,
private static string GetRequestBody(HttpContext context)
{
var bodyStr = "";
var req = context.Request;
//Allows using several time the stream in ASP.Net Core.
req.EnableRewind();
//Important: keep stream opened to read when handling the request.
using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
{
bodyStr = reader.ReadToEnd();
}
// Rewind, so the core is not lost when it looks the body for the request.
req.Body.Position = 0;
return bodyStr;
}
Решение, предоставленное yonisha, чисто, но оно не работает для меня в.Net Core 2.0. Это работает, если у вас есть тело JSON:
public IActionResult MyAction ([FromBody] PayloadObject payloadObject)
{
//create a dictionary to store the json string
var customDataDict = new Dictionary<string, string>();
//convert the object to a json string
string activationRequestJson = JsonConvert.SerializeObject(
new
{
payloadObject = payloadObject
});
customDataDict.Add("body", activationRequestJson);
//Track this event, with the json string, in Application Insights
telemetryClient.TrackEvent("MyAction", customDataDict);
return Ok();
}
Я могу регистрировать тело сообщения запроса в Application Insights с помощью метода @yonisha, но не могу регистрировать тело сообщения ответа. Я заинтересован в регистрации тела ответного сообщения. Я уже регистрирую тело сообщения Post, Put, Delete Request, используя метод @yonisha.
Когда я пытался получить доступ к телу ответа в TelemetryInitializer, я продолжал получать исключение с сообщением об ошибке, в котором говорилось, что "поток не читается. Когда я исследовал больше, я обнаружил, что AzureInitializer работает как часть HttpModule(ApplicationInsightsWebTracking), поэтому к тому времени, когда он получает объект ответа на управление.
Я получил идею из ответа @Oskar. Почему бы не иметь обработчик делегата и не записывать ответ, поскольку объект ответа не удаляется на этапе обработчика сообщений. Обработчик сообщений является частью жизненного цикла веб-API, т.е. аналогичен модулю HTTP, но ограничен веб-API. Когда я разработал и протестировал эту идею, к счастью, она сработала, я записал ответ в сообщение запроса, используя обработчик сообщений, и получил его в AzureInitializer (модуль HTTP, выполнение которого происходит позже, чем обработчик сообщений). Вот пример кода.
public class AzureRequestResponseInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry != null && HttpContext.Current != null && HttpContext.Current.Request != null)
{
if ((HttpContext.Current.Request.HttpMethod == HttpMethod.Post.ToString()
|| HttpContext.Current.Request.HttpMethod == HttpMethod.Put.ToString()) &&
HttpContext.Current.Request.Url.AbsoluteUri.Contains("api"))
using (var reader = new StreamReader(HttpContext.Current.Request.InputStream))
{
HttpContext.Current.Request.InputStream.Position = 0;
string requestBody = reader.ReadToEnd();
if (requestTelemetry.Properties.Keys.Contains("requestbody"))
{
requestTelemetry.Properties["requestbody"] = requestBody;
}
else
{
requestTelemetry.Properties.Add("requestbody", requestBody);
}
}
else if (HttpContext.Current.Request.HttpMethod == HttpMethod.Get.ToString()
&& HttpContext.Current.Response.ContentType.Contains("application/json"))
{
var netHttpRequestMessage = HttpContext.Current.Items["MS_HttpRequestMessage"] as HttpRequestMessage;
if (netHttpRequestMessage.Properties.Keys.Contains("responsejson"))
{
var responseJson = netHttpRequestMessage.Properties["responsejson"].ToString();
if (requestTelemetry.Properties.Keys.Contains("responsebody"))
{
requestTelemetry.Properties["responsebody"] = responseJson;
}
else
{
requestTelemetry.Properties.Add("responsebody", responseJson);
}
}
}
}
}
}
config.MessageHandlers.Add(новый LoggingHandler());
public class LoggingHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith(task =>
{
var response = task.Result;
StoreResponse(response);
return response;
});
}
private void StoreResponse(HttpResponseMessage response)
{
var request = response.RequestMessage;
(response.Content ?? new StringContent("")).ReadAsStringAsync().ContinueWith(x =>
{
var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper;
if (request.Properties.ContainsKey("responseJson"))
{
request.Properties["responsejson"] = x.Result;
}
else
{
request.Properties.Add("responsejson", x.Result);
}
});
}
}
Извините, решение @yonisha не работает в.NET 4.7. Часть Application Insights работает нормально, но на самом деле не существует простого способа получить тело запроса внутри инициализатора телеметрии в.NET 4.7. .NET 4.7 использует GetBufferlessInputStream() для получения потока, и этот поток "читается один раз". Один потенциальный код выглядит так:
private static void LogRequestBody(ISupportProperties requestTelemetry)
{
var requestStream = HttpContext.Current?.Request?.GetBufferlessInputStream();
if (requestStream?.Length > 0)
using (var reader = new StreamReader(requestStream))
{
string body = reader.ReadToEnd();
requestTelemetry.Properties["body"] = body.Substring(0, Math.Min(body.Length, 8192));
}
}
Но возврат из GetBufferlessInputStream() уже рассчитан и не поддерживает поиск. Поэтому тело всегда будет пустой строкой.