Journalisation des requêtes / réponses HTTP brutes dans ASP.NET MVC et IIS7


141

J'écris un service Web (en utilisant ASP.NET MVC) et à des fins de support, nous aimerions pouvoir enregistrer les demandes et les réponses au plus près du format brut, sur le fil (c'est-à-dire avec HTTP méthode, chemin, tous les en-têtes et le corps) dans une base de données.

Ce dont je ne suis pas sûr, c'est de savoir comment obtenir ces données de la manière la moins «mutilée». Je peux reconstituer ce à quoi je pense que la demande ressemble en inspectant toutes les propriétés de l' HttpRequestobjet et en construisant une chaîne à partir d'elles (et de même pour la réponse) mais j'aimerais vraiment obtenir les données de demande / réponse réelles qui sont envoyé sur le fil.

Je suis heureux d'utiliser n'importe quel mécanisme d'interception tel que des filtres, des modules, etc. et la solution peut être spécifique à IIS7. Cependant, je préfère le garder en code géré uniquement.

Des recommandations?

Edit: Je note qu'il y HttpRequesta une SaveAsméthode qui peut enregistrer la demande sur le disque mais cela reconstruit la demande à partir de l'état interne en utilisant une charge de méthodes d'assistance internes qui ne sont pas accessibles publiquement (c'est pourquoi cela ne permet pas d'enregistrer sur un utilisateur fourni stream je ne sais pas). Il semble donc que je devrais faire de mon mieux pour reconstruire le texte de demande / réponse à partir des objets ... gémissement.

Edit 2: Veuillez noter que j'ai dit toute la demande, y compris la méthode, le chemin, les en-têtes, etc. Les réponses actuelles ne regardent que les flux de corps qui n'incluent pas ces informations.

Edit 3: Personne ne lit les questions ici? Cinq réponses à ce jour et pourtant aucune ne fait même allusion à un moyen d'obtenir toute la demande brute sur le fil. Oui, je sais que je peux capturer les flux de sortie et les en-têtes et l'URL et tout ce qui se passe à partir de l'objet de requête. Je l'ai déjà dit dans la question, voir:

Je peux reconstituer ce à quoi je pense que la demande ressemble en inspectant toutes les propriétés de l'objet HttpRequest et en construisant une chaîne à partir d'elles (et de même pour la réponse) mais j'aimerais vraiment obtenir les données de demande / réponse réelles qui est envoyé sur le fil.

Si vous savez que les données brutes complètes (y compris les en-têtes, l'url, la méthode http, etc.) ne peuvent tout simplement pas être récupérées, il serait utile de le savoir. De même, si vous savez comment tout obtenir au format brut (oui, je veux toujours dire y compris les en-têtes, l'url, la méthode http, etc.) sans avoir à le reconstruire, ce que j'ai demandé, alors ce serait très utile. Mais me dire que je peux le reconstruire à partir des HttpRequest/ HttpResponseobjets n'est pas utile. Je le sais. Je l'ai déjà dit.


Remarque: avant que quiconque ne commence à dire que c'est une mauvaise idée, ou limitera l'évolutivité, etc., nous mettrons également en œuvre des mécanismes de limitation, de livraison séquentielle et d'anti-relecture dans un environnement distribué, de sorte que la journalisation de la base de données est de toute façon nécessaire. Je ne cherche pas à savoir si c'est une bonne idée, je cherche comment cela peut être fait.


1
@Kev - Non, c'est un service RESTful implémenté à l'aide d'ASP.NET MVC
Greg Beech

Il est probablement possible d'utiliser IIS7 et un module natif - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna

Avez-vous réussi à mettre en œuvre cela? Juste curieux, avez-vous adopté une stratégie de tampon pour écrire dans db?
systempuntoout

1
Projet intéressant ... si vous finissez par le faire, avec une solution finale?
PreguntonCojoneroCabrón

Réponses:


91

Utilisez définitivement IHttpModuleet implémentez les événements BeginRequestet EndRequest.

Toutes les données "brutes" sont présentes entre HttpRequestet HttpResponse, ce n'est tout simplement pas dans un seul format brut. Voici les parties nécessaires pour créer des vidages de style Fiddler (à peu près aussi proches du HTTP brut que possible):

request.HttpMethod + " " + request.RawUrl + " " + request.ServerVariables["SERVER_PROTOCOL"]
request.Headers // loop through these "key: value"
request.InputStream // make sure to reset the Position after reading or later reads may fail

Pour la réponse:

"HTTP/1.1 " + response.Status
response.Headers // loop through these "key: value"

Notez que vous ne pouvez pas lire le flux de réponse , vous devez donc ajouter un filtre au flux de sortie et capturer une copie.

Dans votre BeginRequest, vous devrez ajouter un filtre de réponse:

HttpResponse response = HttpContext.Current.Response;
OutputFilterStream filter = new OutputFilterStream(response.Filter);
response.Filter = filter;

Magasin filteroù vous pouvez y accéder dans le EndRequestgestionnaire. Je suggère dans HttpContext.Items. Il peut alors obtenir les données de réponse complètes filter.ReadStream().

Ensuite, implémentez en OutputFilterStreamutilisant le modèle Decorator comme wrapper autour d'un flux:

/// <summary>
/// A stream which keeps an in-memory copy as it passes the bytes through
/// </summary>
public class OutputFilterStream : Stream
{
    private readonly Stream InnerStream;
    private readonly MemoryStream CopyStream;

    public OutputFilterStream(Stream inner)
    {
        this.InnerStream = inner;
        this.CopyStream = new MemoryStream();
    }

    public string ReadStream()
    {
        lock (this.InnerStream)
        {
            if (this.CopyStream.Length <= 0L ||
                !this.CopyStream.CanRead ||
                !this.CopyStream.CanSeek)
            {
                return String.Empty;
            }

            long pos = this.CopyStream.Position;
            this.CopyStream.Position = 0L;
            try
            {
                return new StreamReader(this.CopyStream).ReadToEnd();
            }
            finally
            {
                try
                {
                    this.CopyStream.Position = pos;
                }
                catch { }
            }
        }
    }


    public override bool CanRead
    {
        get { return this.InnerStream.CanRead; }
    }

    public override bool CanSeek
    {
        get { return this.InnerStream.CanSeek; }
    }

    public override bool CanWrite
    {
        get { return this.InnerStream.CanWrite; }
    }

    public override void Flush()
    {
        this.InnerStream.Flush();
    }

    public override long Length
    {
        get { return this.InnerStream.Length; }
    }

    public override long Position
    {
        get { return this.InnerStream.Position; }
        set { this.CopyStream.Position = this.InnerStream.Position = value; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return this.InnerStream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        this.CopyStream.Seek(offset, origin);
        return this.InnerStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        this.CopyStream.SetLength(value);
        this.InnerStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        this.CopyStream.Write(buffer, offset, count);
        this.InnerStream.Write(buffer, offset, count);
    }
}

1
Bonne réponse. Un commentaire cependant: vous avez dit "Il peut alors obtenir les données de réponse complètes dans filter.ToString ()." -ne voulez-vous pas dire filter.ReadStream ()? (J'implémente dans vb.net pas c # mais si j'exécute ToString, j'obtiens simplement le nom de la classe sous forme de chaîne. .ReadStream renvoie le corps de réponse souhaité.
Adam

Je suis d'accord, bonne réponse. Je l'ai utilisé comme base pour un enregistreur personnalisé, mais j'ai maintenant rencontré un problème où certains en-têtes sont manquants et, plus important encore, lors de l'utilisation de la compression IIS, je ne peux pas accéder à la réponse compressée finale. J'ai commencé une nouvelle question connexe ( stackoverflow.com/questions/11084459/… ) pour cela.
Chris

2
Je pense que mckamey est un génie. Pouvez-vous aller travailler pour Microsoft afin que nous obtenions simplement des solutions intelligentes au lieu d'avoir besoin de solutions de contournement brillantes?
Abacus

4
Attention, request.RawUrl peut déclencher une exception de validation de requête. Dans 4.5, vous pouvez utiliser request.Unvalidated.RawUrl pour éviter cela. En 4.0, j'ai fini par utiliser une réflexion pour imiter Request.SaveAs
Freek

1
@mckamey J'essaie d'implémenter votre solution dans mon global.asax avec Application_BeginRequest et Application_EndRequest mais je ne sais pas quel code dois-je écrire dans EndRequest, pouvez-vous fournir un exemple dans votre réponse plz?
Jerome2606

48

La méthode d'extension suivante sur HttpRequest créera une chaîne qui peut être collée dans Fiddler et rejouée.

namespace System.Web
{
    using System.IO;

    /// <summary>
    /// Extension methods for HTTP Request.
    /// <remarks>
    /// See the HTTP 1.1 specification http://www.w3.org/Protocols/rfc2616/rfc2616.html
    /// for details of implementation decisions.
    /// </remarks>
    /// </summary>
    public static class HttpRequestExtensions
    {
        /// <summary>
        /// Dump the raw http request to a string. 
        /// </summary>
        /// <param name="request">The <see cref="HttpRequest"/> that should be dumped.       </param>
        /// <returns>The raw HTTP request.</returns>
        public static string ToRaw(this HttpRequest request)
        {
            StringWriter writer = new StringWriter();

            WriteStartLine(request, writer);
            WriteHeaders(request, writer);
            WriteBody(request, writer);

            return writer.ToString();
        }

        private static void WriteStartLine(HttpRequest request, StringWriter writer)
        {
            const string SPACE = " ";

            writer.Write(request.HttpMethod);
            writer.Write(SPACE + request.Url);
            writer.WriteLine(SPACE + request.ServerVariables["SERVER_PROTOCOL"]);
        }

        private static void WriteHeaders(HttpRequest request, StringWriter writer)
        {
            foreach (string key in request.Headers.AllKeys)
            {
                writer.WriteLine(string.Format("{0}: {1}", key, request.Headers[key]));
            }

            writer.WriteLine();
        }

        private static void WriteBody(HttpRequest request, StringWriter writer)
        {
            StreamReader reader = new StreamReader(request.InputStream);

            try
            {
                string body = reader.ReadToEnd();
                writer.WriteLine(body);
            }
            finally
            {
                reader.BaseStream.Position = 0;
            }
        }
    }
}

5
Très bon code! Mais pour que cela fonctionne avec MVC 4 je devais changer le nom de classe HttpRequestBaseExtensionset de changer HttpRequestpour HttpRequestBaseà chaque endroit.
Dmitry

35

Vous pouvez utiliser la variable serveur ALL_RAW pour obtenir les en-têtes HTTP d'origine envoyés avec la requête, puis vous pouvez obtenir le InputStream comme d'habitude:

string originalHeader = HttpHandler.Request.ServerVariables["ALL_RAW"];

consultez: http://msdn.microsoft.com/en-us/library/ms524602%28VS.90%29.aspx


Cela a fonctionné pour moi aussi. Je n'avais même pas besoin d'être dans un gestionnaire. J'ai pu toujours y accéder depuis la page.
Helephant

3
Ou dans le contexte du serveur ASP.NET, utilisez: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar

Je ne parviens pas à obtenir le corps de la requête depuis Request.InputStream, il renvoie "" pour moi à chaque fois, cependant ALL_RAW fonctionne très bien pour renvoyer les en-têtes de requête, donc cette réponse est à moitié correcte.
Justin

1
Vous pouvez également utiliser HttpContext.Current.Requestpour récupérer le contexte actuel en dehors des contrôleurs MVC, des pages ASPX, etc ... assurez-vous simplement qu'il n'est pas nul en premier;)
jocull

16

Eh bien, je travaille sur un projet et j'ai fait, peut-être pas trop profond, un journal en utilisant les paramètres de requête:

Regarde:

public class LogAttribute : ActionFilterAttribute
{
    private void Log(string stageName, RouteData routeData, HttpContextBase httpContext)
    {
        //Use the request and route data objects to grab your data
        string userIP = httpContext.Request.UserHostAddress;
        string userName = httpContext.User.Identity.Name;
        string reqType = httpContext.Request.RequestType;
        string reqData = GetRequestData(httpContext);
        string controller = routeData["controller"];
        string action = routeData["action"];

        //TODO:Save data somewhere
    }

    //Aux method to grab request data
    private string GetRequestData(HttpContextBase context)
    {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < context.Request.QueryString.Count; i++)
        {
            sb.AppendFormat("Key={0}, Value={1}<br/>", context.Request.QueryString.Keys[i], context.Request.QueryString[i]);
        }

        for (int i = 0; i < context.Request.Form.Count; i++)
        {
            sb.AppendFormat("Key={0}, Value={1}<br/>", context.Request.Form.Keys[i], context.Request.Form[i]);
        }

        return sb.ToString();
    }

Vous pouvez décorer votre classe de contrôleurs pour la consigner entièrement:

[Log]
public class TermoController : Controller {...}

ou enregistrez juste quelques méthodes d'action individuelles

[Log]
public ActionResult LoggedAction(){...}

12

Avez-vous besoin de le conserver dans un code géré?

Il convient de mentionner que vous pouvez activer la journalisation des échecs de suivi dans IIS7 si vous n'aimez pas réinventer la roue. Cela enregistre les en-têtes, le corps de la requête et de la réponse ainsi que de nombreuses autres choses.

Echec de la journalisation du suivi


Et si ce n'est pas un échec?
Sinaesthetic

7
Vous pouvez également utiliser la journalisation du
suivi des

2
C'est de loin la solution la plus simple.
Kehlan Krumme

Pour voir toute la trace de la pile, il était nécessaire d'ajouter GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;à la fin de Register(..)in WebApiConfig.cs, mais cela peut varier entre les versions.
Evgeni Sergeev

8

J'ai suivi l'approche de McKAMEY. Voici un module que j'ai écrit qui vous permettra de démarrer et, espérons-le, vous fera gagner du temps. Vous devrez évidemment brancher le Logger avec quelque chose qui fonctionne pour vous:

public class CaptureTrafficModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
        context.EndRequest += new EventHandler(context_EndRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;

        OutputFilterStream filter = new OutputFilterStream(app.Response.Filter);
        app.Response.Filter = filter;

        StringBuilder request = new StringBuilder();
        request.Append(app.Request.HttpMethod + " " + app.Request.Url);
        request.Append("\n");
        foreach (string key in app.Request.Headers.Keys)
        {
            request.Append(key);
            request.Append(": ");
            request.Append(app.Request.Headers[key]);
            request.Append("\n");
        }
        request.Append("\n");

        byte[] bytes = app.Request.BinaryRead(app.Request.ContentLength);
        if (bytes.Count() > 0)
        {
            request.Append(Encoding.ASCII.GetString(bytes));
        }
        app.Request.InputStream.Position = 0;

        Logger.Debug(request.ToString());
    }

    void context_EndRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        Logger.Debug(((OutputFilterStream)app.Response.Filter).ReadStream());
    }

    private ILogger _logger;
    public ILogger Logger
    {
        get
        {
            if (_logger == null)
                _logger = new Log4NetLogger();
            return _logger;
        }
    }

    public void Dispose()
    {
        //Does nothing
    }
}

3
Vous ne pouvez pas convertir en toute sécurité app.Response.Filter vers autre chose qu'un Stream. D'autres HttpModules peuvent envelopper votre filtre de réponse avec le leur et dans ce cas, vous obtiendrez une exception de conversion non valide.
Micah Zoltu

Ne devrait-il pas l'être Encoding.UTF8, ou peut Encoding.Default- être lors de la lecture du flux de demande? Ou utilisez simplement un StreamReader( avec des mises en garde )
drzaus

5

OK, il semble donc que la réponse soit "non, vous ne pouvez pas obtenir les données brutes, vous devez reconstruire la requête / réponse à partir des propriétés des objets analysés". Eh bien, j'ai fait le truc de la reconstruction.


3
Avez-vous vu le commentaire de Vineus sur ServerVariables ["ALL_RAW"]? Je ne l'ai pas encore essayé moi-même, mais il est documenté pour renvoyer les informations d'en-tête brutes exactement comme envoyées par le client. Même si le document s'avère être faux, et qu'il fait une reconstruction, bon, reconstruction gratuite :-)
Jonathan Gilbert

3

utilisez un IHttpModule :

    namespace Intercepts
{
    class Interceptor : IHttpModule
    {
        private readonly InterceptorEngine engine = new InterceptorEngine();

        #region IHttpModule Members

        void IHttpModule.Dispose()
        {
        }

        void IHttpModule.Init(HttpApplication application)
        {
            application.EndRequest += new EventHandler(engine.Application_EndRequest);
        }
        #endregion
    }
}

    class InterceptorEngine
    {       
        internal void Application_EndRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;

            HttpResponse response = application.Context.Response;
            ProcessResponse(response.OutputStream);
        }

        private void ProcessResponse(Stream stream)
        {
            Log("Hello");
            StreamReader sr = new StreamReader(stream);
            string content = sr.ReadToEnd();
            Log(content);
        }

        private void Log(string line)
        {
            Debugger.Log(0, null, String.Format("{0}\n", line));
        }
    }

3
D'après Alex et ma propre expérience, je ne pense pas que vous puissiez lire à partir de HttpResponse.OutputStream, donc votre méthode de journalisation dans la méthode ProcessResponse ne fonctionnera probablement pas.
William Gross

2
William a raison. HttpResponse.OutputStream n'est pas lisible. Je trouve une solution qui consiste à utiliser HttpResponse.Filter et à remplacer le flux de sortie par défaut par le vôtre.
Eric Fan


3

si pour une utilisation occasionnelle, pour contourner un virage serré, que diriez-vous de quelque chose de brut comme ci-dessous?

Public Function GetRawRequest() As String
    Dim str As String = ""
    Dim path As String = "C:\Temp\REQUEST_STREAM\A.txt"
    System.Web.HttpContext.Current.Request.SaveAs(path, True)
    str = System.IO.File.ReadAllText(path)
    Return str
End Function

1

Vous pouvez accomplir cela dans un DelegatingHandlersans utiliser le OutputFiltermentionné dans d'autres réponses dans .NET 4.5 à l'aide de la Stream.CopyToAsync()fonction.

Je ne suis pas sûr des détails, mais cela ne déclenche pas toutes les mauvaises choses qui se produisent lorsque vous essayez de lire directement le flux de réponse.

Exemple:

public class LoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        DoLoggingWithRequest(request);
        var response = await base.SendAsync(request, cancellationToken);
        await DoLoggingWithResponse(response);
        return response;
    }

    private async Task DologgingWithResponse(HttpResponseMessage response) {
        var stream = new MemoryStream();
        await response.Content.CopyToAsync(stream).ConfigureAwait(false);     
        DoLoggingWithResponseContent(Encoding.UTF8.GetString(stream.ToArray()));

        // The rest of this call, the implementation of the above method, 
        // and DoLoggingWithRequest is left as an exercise for the reader.
    }
}

0

Je sais que ce n'est pas du code géré, mais je vais suggérer un filtre ISAPI. Cela fait quelques années que j'ai eu le "plaisir" de maintenir mon propre ISAPI mais d'après ce que je me souviens, vous pouvez avoir accès à tout cela, à la fois avant et après qu'ASP.Net l'ait fait.

http://msdn.microsoft.com/en-us/library/ms524610.aspx

Si un HTTPModule n'est pas assez bon pour ce dont vous avez besoin, alors je ne pense tout simplement pas qu'il existe un moyen géré de le faire avec la quantité de détails requise. Mais ça va être pénible à faire.



0

Il peut être préférable de le faire en dehors de votre application. Vous pouvez configurer un proxy inverse pour faire des choses comme celle-ci (et bien plus encore). Un proxy inverse est essentiellement un serveur Web qui se trouve dans votre salle de serveurs et se situe entre vos serveurs Web et le client. Voir http://en.wikipedia.org/wiki/Reverse_proxy


0

D'accord avec FigmentEngine, IHttpModulesemble être la voie à suivre.

Regardez dans httpworkerrequest, readentitybodyet GetPreloadedEntityBody.

Pour obtenir le, httpworkerrequestvous devez faire ceci:

(HttpWorkerRequest)inApp.Context.GetType().GetProperty("WorkerRequest", bindingFlags).GetValue(inApp.Context, null);

inAppest l'objet httpapplication.


1
J'ai déjà dit que cette réponse ne convenait pas car elle ne saisissait pas la plupart des informations que j'avais demandées. En quoi cette réponse est-elle utile de quelque manière que ce soit?
Greg Beech

Plus d'expliquer, en quoi cette réponse est-elle utile de quelque manière que ce soit?
PreguntonCojoneroCabrón

0

HttpRequestet HttpResponseavant MVC avait un GetInputStream()et GetOutputStream()qui pouvait être utilisé à cette fin. Je n'ai pas examiné ces éléments dans MVC, donc je ne suis pas sûr qu'ils soient disponibles mais cela pourrait être une idée :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.