2017-10-04 10 views
0

TL; DR Question: Y at-il un moyen d'obtenir le corps de la requête dans une instance ExceptionTelemetry existante, dans ASP.NET de base, sans copier tous les corps de demande?Comment enrichir App Insights ExceptionTelemetry avec le corps de la demande dans ASP.NET base

Je voudrais être en mesure d'inclure le Corps de la demande dans la télémétrie d'exception pour avoir un aperçu de l'application. C'est à dire. Je veux seulement la demande quand une exception s'est produite. En recherchant la documentation sur ASP.NET Core et Application Insights, il semble que la "bonne" façon d'enrichir la télémétrie soit d'utiliser TelemetryProcessors ou TelemetryInitializers, j'ai donc essayé d'obtenir le corps de la requête dans un telemetryinitializer personnalisé, seulement pour découvrir que le flux de corps de la demande est fermé/disposé quand je veux lire (rebobinage ne permet pas, car apparemment il a déjà été disposé lorsque l'Insights App telemetryinitializer est en cours d'exécution).

Je fini par résoudre en ayant un middleware qui copie le flux de demande:

public async Task Invoke(HttpContext context) 
{ 
    var stream = context.Request.Body; 

    try 
    { 
     using (var buffer = new MemoryStream()) 
     { 
      // Copy the request stream and rewind the copy 
      await stream.CopyToAsync(buffer); 
      buffer.Position = 0L; 

      // Create another copy and rewind both 
      var otherBuffer = new MemoryStream(); 
      await buffer.CopyToAsync(otherBuffer); 
      buffer.Position = 0L; 
      otherBuffer.Position = 0L; 

      // Replace the request stream by the first copy 
      context.Request.Body = buffer; 

      // Put a separate copy in items collection for other things to use 
      context.Items["RequestStreamCopy"] = otherBuffer; 
      context.Response.RegisterForDispose(otherBuffer); 

      await next(context); 
     } 
    } 
    finally 
    { 
     context.Request.Body = stream; 
    } 
} 

Et mon initialiseur:

public AiExceptionInitializer(IHttpContextAccessor httpContextAccessor) 
{ 
    this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor"); 
} 

public void Initialize(ITelemetry telemetry) 
{ 
    var context = this.httpContextAccessor.HttpContext; 

    if (context == null) 
    { 
     return; 
    } 

    lock (context) 
    { 
     var request = context.Features.Get<RequestTelemetry>(); 
     if (request == null) 
     { 
      return; 
     } 

     this.OnInitializeTelemetry(context, request, telemetry); 
    } 
} 

protected void OnInitializeTelemetry(HttpContext platformContext, RequestTelemetry requestTelemetry, ITelemetry telemetry) 
{ 
    if (telemetry is ExceptionTelemetry exceptionTelemetry) 
    { 
     var stream = platformContext.Items["RequestStreamCopy"] as MemoryStream; 

     try 
     { 
      if (stream?.Length <= 0) 
      { 
       return; 
      } 

      // Rewind the stream position just to be on the safe side 
      stream.Position = 0L; 

      using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true)) 
      { 
       string requestBody = reader.ReadToEnd(); 
       exceptionTelemetry.Properties.Add("HttpRequestBody", requestBody); 
      } 
     } 
     finally 
     { 
      if (stream != null) 
      { 
       // Rewind the stream for others to use. 
       stream.Position = 0L; 
      } 
     } 
    } 
} 

Toutefois, ce devoir copier le flux de demande (DEUX FOIS) pour chaque demande, de ne l'avoir utilisé que sur des échecs me semble plutôt inefficace. Je me demande s'il y a une autre façon de faire quelque chose comme ça où je n'ai pas de copier le flux de chaque demande juste pour sérialiser les défaillants? Je suis conscient que je pourrais "simplement" écrire un middleware qui créerait de nouvelles instances ExceptionTelemetry, mais pour autant que je sache (je me trompe peut-être), il me laisserait deux instances Exception dans Application Insights (ie celui généré par moi et celui généré par les extensions AI), au lieu d'une seule exception avec la propriété ajoutée dont j'ai besoin.

+0

je pourrais peut-être mettre le flux en fonction au lieu, pas sûr quand je dois faire un sur l'autre, mais qui est hors de propos pour la question que je deviner. –

+0

Sera-t-il possible d'écrire un intergiciel de suivi des exceptions mais de l'utiliser uniquement pour capturer le corps de la requête dans le contexte? (Paraphraser: est le corps de la demande toujours disponible dans le middleware de suivi d'exception personnalisée?) Si oui - vous pouvez écrire ce corps dans le contexte que dans cet middleware d'exception, puis, dans initialiseur de télémétrie, vous serez en mesure de vérifier si le corps n'est pas null dans le contexte - utilisez-le pour remplir les propriétés d'exception. Ainsi, en ordonnant votre middleware avant le middleware AI - vous placez le contexte juste avant l'initialisation. –

+0

Pas tout à fait sûr, mais je vais essayer et vous le faire savoir. –

Répondre

0

Merci au commentaire de @DmitryMatveev je trouve une solution de rechange. Je ne suis pas sûr que ce soit le plus efficace, mais c'est mieux que ce que j'avais!

Le middleware est « réduit » seulement des exceptions de suivi et sérialisation puis le corps tout de suite (vous pourriez encore avoir une copie de flux, mais je ne ai pas besoin dans mon cas), quelque chose comme ce qui suit:

using System; 
using System.IO; 
using System.Text; 
using System.Threading.Tasks; 
using Microsoft.AspNetCore.Http; 
using Microsoft.AspNetCore.Http.Internal; 

public class ExceptionBodyTrackingMiddleware 
{ 
    public const string ExceptionRequestBodyKey = "ExceptionRequestBody"; 
    private readonly RequestDelegate next; 

    public ExceptionBodyTrackingMiddleware(RequestDelegate next) 
    { 
     this.next = next ?? throw new ArgumentNullException(nameof(next)); 
    } 

    public async Task Invoke(HttpContext context) 
    { 
     try 
     { 
      context.Request.EnableRewind(); 
      await this.next.Invoke(context); 
     } 
     catch (Exception) 
     { 
      RegisterRequestBody(context); 

      throw; 
     } 
    } 

    private static void RegisterRequestBody(HttpContext context) 
    { 
     if (context.Request.Body?.CanSeek == false) 
     { 
      return; 
     } 

     var body = CopyStreamToString(context.Request.Body); 
     context.Items[ExceptionRequestBodyKey] = body; 
    } 

    private static string CopyStreamToString(Stream stream) 
    { 
     var originalPosition = stream.Position; 
     RewindStream(stream); 
     string requestBody = null; 

     using (var reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true)) 
     { 
      requestBody = reader.ReadToEnd(); 
     } 
     stream.Position = originalPosition; 
     return requestBody; 
    } 

    private static void RewindStream(Stream stream) 
    { 
     if (stream != null) 
     { 
      stream.Position = 0L; 
     } 
    } 
} 

de même, le Initializer devient beaucoup plus simple:

public AiExceptionInitializer(IHttpContextAccessor httpContextAccessor) 
{ 
    this.httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException("httpContextAccessor"); 
} 

public void Initialize(ITelemetry telemetry) 
{ 
    var context = this.httpContextAccessor.HttpContext; 

    if (context == null) 
    { 
     return; 
    } 

    lock (context) 
    { 
     var request = context.Features.Get<RequestTelemetry>(); 
     if (request == null) 
     { 
      return; 
     } 

     this.OnInitializeTelemetry(context, request, telemetry); 
    } 
} 

protected void OnInitializeTelemetry(HttpContext platformContext, RequestTelemetry requestTelemetry, ITelemetry telemetry) 
{ 
    if (telemetry is ExceptionTelemetry exceptionTelemetry) 
    { 
     var requestBody = platformContext.Items[ExceptionBodyTrackingMiddleware.ExceptionRequestBodyKey] as string; 
     exceptionTelemetry.Properties.Add("HttpRequestBody", requestBody); 
    } 
}