Можно ли просмотреть тело запроса POST в Application Insights?
Я могу видеть детали запроса, но не полезную нагрузку, публикуемую в приложениях. Должен ли я отслеживать это с помощью некоторого кодирования?
Я создаю ядро MVC 1.1 Web Api.
Можно ли просмотреть тело запроса POST в Application Insights?
Я могу видеть детали запроса, но не полезную нагрузку, публикуемую в приложениях. Должен ли я отслеживать это с помощью некоторого кодирования?
Я создаю ядро MVC 1.1 Web Api.
Вы можете просто реализовать свой собственный инициализатор телеметрии:
Например, ниже реализации, которая извлекает полезную нагрузку и добавляет ее в качестве пользовательского измерения телеметрии запроса:
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);
}
}
}
Тогда также обязательно добавьте это в свой Автозагрузка → ConfigureServices
services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();
EDIT:
Если вы также хотите получить тело ответа, я считаю полезным создать часть промежуточного программного обеспечения (.NET Core, не уверен насчет Framework). Сначала я применил описанный выше подход, при котором вы регистрируете ответ и запрос, но большую часть времени вы хотите получить их вместе:
public async Task Invoke(HttpContext context)
{
var reqBody = await this.GetRequestBodyForTelemetry(context.Request);
var respBody = await this.GetResponseBodyForTelemetry(context);
this.SendDataToTelemetryLog(reqBody, respBody, context);
}
Это ожидает как запрос, так и ответ. GetRequestBodyForTelemetry
практически идентичен коду из инициализатора телеметрии, за исключением использования Task
. Для тела ответа я использовал приведенный ниже код, я также исключил 204, поскольку это приводит к нулевому значению:
public async Task<string> GetResponseBodyForTelemetry(HttpContext context)
{
var 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;
}
}
Несколько дней назад у меня появилось аналогичное требование регистрировать основную часть запроса в приложениях с фильтрацией конфиденциальных входных пользовательских данных из полезной нагрузки. Так что делюсь своим решением. Приведенное ниже решение разработано для веб-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
атрибут 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'.
Я реализовал для этого промежуточное ПО,
Вызывается метод,
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();
}
Извините, решение @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() уже используется и не поддерживает поиск. Поэтому тело всегда будет пустой строкой.
Я так и не получил работающий ответ @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()
- это метод расширения из Microsoft.ApplicationInsights.Web.
Я могу записать тело сообщения запроса в Application Insights с помощью метода @yonisha, но не могу записать тело сообщения ответа. Я заинтересован в регистрации тела сообщения ответа. Я уже регистрирую тело сообщения Post, Put, Delete Request, используя метод @yonisha.
Когда я пытался получить доступ к телу ответа в TelemetryInitializer, я получал исключение с сообщением об ошибке, в котором говорилось, что "поток не читается. Когда я исследовал больше, я обнаружил, что AzureInitializer работает как часть HttpModule (ApplicationInsightsWebTracking), поэтому к тому времени получает ответный объект управления.
Я получил идею от ответа @Oskar. Почему бы не иметь обработчик делегата и записать ответ, так как объект ответа не расположен на этапе обработчика сообщения. Обработчик сообщений является частью жизненного цикла Web 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(new 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);
}
});
}
}