web-dev-qa-db-fra.com

Vue POST corps de la demande dans Application Insights

Est-il possible d'afficher le corps de la demande POST dans Application Insights?

Je peux voir les détails de la demande, mais pas la charge utile affichée dans les informations sur les applications. Dois-je suivre cela avec un code?

Je construis un Api Web 1.1 de base MVC. 

 POST request

33
Dhanuka777

Vous pouvez simplement implémenter votre propre initialisateur de télémétrie :

Par exemple, ci-dessous une implémentation qui extrait la charge utile et l'ajoute en tant que dimension personnalisée de la télémétrie de la demande:

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);
            }
        }
    }
}

Puis ajoutez-le à la configuration par fichier de configuration ou via le code:

TelemetryConfiguration.Active.TelemetryInitializers.Add(new RequestBodyInitializer());

Puis interrogez-le dans Analytics:

requests | limit 1 | project customDimensions.body
25
yonisha

La solution fournie par @yonisha est à mon avis la plus propre disponible. Cependant, vous devez toujours insérer votre httpcontext et pour cela, vous avez besoin de plus de code. J'ai également inséré des commentaires basés sur ou extraits des exemples de code ci-dessus. Il est important de réinitialiser la position de votre demande, sinon vous perdrez ses données.

C'est ma solution que j'ai testée et qui me donne le 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);
            }
        }
    }

Ensuite, veillez également à l'ajouter à votre démarrage -> ConfigureServices

services.AddSingleton<ITelemetryInitializer, RequestBodyInitializer>();

MODIFIER:

Si vous souhaitez également obtenir le corps de réponse, j’ai trouvé utile de créer un middleware (le noyau dotnet n’est pas sûr du framework). Au début, j’ai adopté l’approche ci-dessus, dans laquelle vous enregistrez une réponse et une demande, mais la plupart du temps, vous souhaitez les regrouper.

    public async Task Invoke(HttpContext context)
    {
        var reqBody = await this.GetRequestBodyForTelemetry(context.Request);

        var respBody = await this.GetResponseBodyForTelemetry(context);
        this.SendDataToTelemetryLog(reqBody, respBody, context);
    }

Cela attend à la fois une requête et une réponse où la requête est à peu près la même que celle décrite ci-dessus, au lieu qu'il s'agisse d'une tâche. 

Pour le corps de réponse que j'ai utilisé le code ci-dessous, j'ai également exclu un 204 car cela mène à un nullref:

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;
        }
    }
10
joerivrij

J'ai mis en place un middleware pour cela, 

La méthode Invoke fait,

 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);
        }

Où, GetRequestBody est comme,

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;
    }
2
Dhanuka777

La solution fournie par yonisha est propre, mais elle ne fonctionne pas pour moi dans .Net Core 2.0. Cela fonctionne si vous avez un corps 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();
}
1
paulyb

Il y a quelques jours, j'ai eu la même exigence de journaliser la demande Body in Application insights avec le filtrage des données utilisateur d'entrée sensibles de la charge utile. Alors partage ma solution. La solution ci-dessous est développée pour l’API Web ASP.NET Core 2.0.

ActionFilterAttribute

J'ai utilisé ActionFilterAttribute de (Microsoft.AspNetCore.Mvc.Filters namespace) qui fournit le modèle via ActionArgument afin que, par réflexion, ces propriétés puissent être extraites et marquées comme sensibles.

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();
    }
}

Le 'LogActionFilterAttribute' est injecté dans le pipeline MVC en tant que filtre.

 services.AddMvc(options =>
 {
       options.Filters.Add<LogActionFilterAttribute>();
 });

NoLogAttribute

Dans le code ci-dessus, l'attribut NoLogAttribute est utilisé et doit être appliqué dans les propriétés Modèle/Modèle ou le paramètre de méthode pour indiquer que la valeur ne doit pas être consignée.

public class NoLogAttribute : Attribute
{
}

NoPIILogContractResolver

De plus, NoPIILogContractResolver est utilisé dans JsonSerializerSettings au cours du processus de sérialisation

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 

Pour injecter l'objet RequestTelemetry, je dois utiliser ITelemetryInitializer afin que RequestTelemetry puisse être récupéré dans la classe 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);
            }
        }
    }
}

La PIITelemetryInitializer est enregistrée comme 

services.AddSingleton<ITelemetryInitializer, PIITelemetryInitializer>();

Fonction de test

Le code suivant illustre l'utilisation du code ci-dessus

Créé un contrôleur

[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 Modèle est défini comme 

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; }
}

Alors, quand l'API est appelée par l'outil Swagger

 enter image description here

Le jsonBody est connecté à Request sans données sensibles. Toutes les données sensibles sont remplacées par le littéral de chaîne 'PII Data'.

 enter image description here

0
user1672994

Je suis désolé, la solution de @ yonisha ne semble pas fonctionner dans .NET 4.7. La partie Application Insights fonctionne correctement, mais il n’existe aucun moyen simple d’obtenir le corps de la demande dans l’initialiseur de télémétrie dans .NET 4.7. .NET 4.7 utilise GetBufferlessInputStream () pour obtenir le flux, et ce flux est "lu une fois". Un code potentiel est comme ceci: 

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));
        }
}

Mais le retour de GetBufferlessInputStream () est déjà consommé et ne prend pas en charge la recherche. Par conséquent, le corps sera toujours une chaîne vide.

0
Ivan Uthus

La réponse de @ yonisha n'ayant jamais fonctionné, j'ai utilisé à la place une 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() est une méthode d'extension de Microsoft.ApplicationInsights.Web .

0
Oskar