Logging permintaan HTTP mentah / respons di ASP.NET MVC & IIS7

140

Saya sedang menulis layanan web (menggunakan ASP.NET MVC) dan untuk tujuan dukungan kami ingin dapat mencatat permintaan dan tanggapan sedekat mungkin dengan format mentah, on-the-wire (yaitu termasuk HTTP metode, path, semua header, dan tubuh) ke dalam database.

Apa yang saya tidak yakin adalah bagaimana mendapatkan data ini dengan cara yang paling 'rusak'. Saya dapat membentuk kembali seperti apa yang saya yakini seperti permintaan dengan memeriksa semua properti dariHttpRequest objek dan membangun string dari mereka (dan juga untuk respon) tetapi saya benar-benar ingin mendapatkan data permintaan / respons aktual yang dikirim melalui kawat.

Saya senang menggunakan mekanisme intersepsi seperti filter, modul, dll. Dan solusinya dapat spesifik untuk IIS7. Namun, saya lebih suka menyimpannya dalam kode terkelola saja.

Ada rekomendasi?

Sunting: Saya perhatikan bahwa HttpRequestmemiliki SaveAsmetode yang dapat menyimpan permintaan ke disk tetapi ini merekonstruksi permintaan dari kondisi internal menggunakan beban metode pembantu internal yang tidak dapat diakses secara publik (cukup mengapa ini tidak memungkinkan menyimpan ke yang disediakan pengguna) aliran saya tidak tahu). Jadi itu mulai terlihat seperti saya harus melakukan yang terbaik untuk merekonstruksi teks permintaan / respons dari objek ... erangan.

Sunting 2: Harap perhatikan bahwa saya mengatakan seluruh permintaan termasuk metode, jalur, tajuk dll. Respons saat ini hanya melihat aliran tubuh yang tidak termasuk informasi ini.

Sunting 3: Apakah tidak ada yang membaca pertanyaan di sekitar sini? Sejauh ini lima jawaban namun tidak ada satu pun petunjuk tentang cara untuk mendapatkan seluruh permintaan mentah on-the-wire. Ya, saya tahu saya bisa menangkap aliran keluaran dan tajuk serta URL dan semua itu dari objek permintaan. Saya sudah mengatakan itu dalam pertanyaan, lihat:

Saya dapat membentuk kembali apa yang saya yakini seperti permintaan dengan memeriksa semua properti dari objek HttpRequest dan membuat string dari mereka (dan juga untuk respon) tetapi saya benar-benar ingin mendapatkan data request / response yang sebenarnya yang dikirim pada kabel.

Jika Anda mengetahui data mentah lengkap (termasuk tajuk, url, metode http, dll.) Tidak dapat diambil maka itu akan berguna untuk diketahui. Demikian pula jika Anda tahu cara mendapatkan semuanya dalam format mentah (ya, maksud saya masih termasuk header, url, metode http, dll.) Tanpa harus merekonstruksi itu, yang saya tanyakan, maka itu akan sangat berguna. Tetapi mengatakan kepada saya bahwa saya dapat merekonstruksinya dari HttpRequest/ HttpResponseobjek tidak berguna. Saya tahu itu. Saya sudah mengatakannya.


Harap dicatat: Sebelum ada yang mulai mengatakan ini adalah ide yang buruk, atau akan membatasi skalabilitas, dll., Kami juga akan menerapkan mekanisme pelambatan, pengiriman berurutan, dan anti-ulangan di lingkungan terdistribusi, sehingga pembuatan basis data tetap diperlukan. Saya tidak mencari diskusi apakah ini ide yang baik, saya sedang mencari cara untuk melakukannya.

Greg Beech
sumber
1
@Kev - Tidak, itu adalah layanan tenang diimplementasikan menggunakan ASP.NET MVC
Greg Beech
Mungkin dimungkinkan untuk menggunakan IIS7 dan modul asli - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna
Sudahkah Anda menerapkan ini? Hanya ingin tahu, apakah Anda mengadopsi strategi buffer untuk menulis ke db?
systempuntoout
1
Proyek yang menarik ... jika Anda akhirnya melakukannya, dengan solusi akhir?
PreguntonCojoneroCabrón

Jawaban:

91

Pasti menggunakan IHttpModuledan menerapkan BeginRequestdan EndRequestacara.

Semua data "mentah" ada di antaranya HttpRequestdan HttpResponse, itu tidak dalam format mentah tunggal. Berikut adalah bagian-bagian yang diperlukan untuk membangun dump gaya-Fiddler (sedekat mungkin dengan HTTP mentah):

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

Untuk tanggapan:

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

Perhatikan bahwa Anda tidak dapat membaca aliran respons sehingga Anda harus menambahkan filter ke aliran Output dan mengambil salinan.

Di Anda BeginRequest, Anda perlu menambahkan filter tanggapan:

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

Simpan di filtermana Anda bisa mendapatkannya di EndRequesthandler. Saya sarankan di HttpContext.Items. Di sana kemudian bisa mendapatkan data respons penuh filter.ReadStream().

Kemudian implementasikan OutputFilterStreammenggunakan pola Dekorator sebagai pembungkus di sekitar aliran:

/// <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);
    }
}
mckamey
sumber
1
Jawaban bagus. Namun satu komentar: Anda berkata "Di sana kemudian bisa mendapatkan data respons lengkap di filter.ToString ()." -Apakah maksud Anda filter.ReadStream ()? (Saya menerapkan di vb.net bukan c # tetapi jika saya menjalankan ToString saya hanya mendapatkan nama kelas sebagai string. .ReadStream mengembalikan tubuh respons yang diinginkan.
Adam
Saya setuju, jawaban yang bagus. Saya telah menggunakannya sebagai dasar untuk logger kustom tetapi sekarang telah menemukan masalah di mana beberapa header tidak ada dan yang paling penting ketika menggunakan kompresi IIS saya tidak dapat mengakses respon terkompresi akhir. Saya memulai pertanyaan terkait baru ( stackoverflow.com/questions/11084459/… ) untuk ini.
Chris
2
Saya pikir mckamey adalah seorang jenius. Bisakah Anda bekerja untuk Microsoft sehingga kami hanya mendapatkan solusi cerdas daripada harus memiliki solusi yang brilian?
Abacus
4
Waspadalah terhadap permintaan itu. RawUrl dapat memicu pengecualian validasi permintaan. Di 4,5 Anda dapat menggunakan request.Unvalidated.RawUrl untuk mencegah ini. Pada 4.0 saya akhirnya menggunakan beberapa refleksi untuk meniru Request.SaveAs
Freek
1
@ mckamey Saya mencoba menerapkan solusi Anda di global.asax saya dengan Application_BeginRequest dan Application_EndRequest tapi saya tidak yakin kode apa yang harus saya tulis di EndRequest, dapatkah Anda memberikan contoh dalam jawaban Anda?
Jerome2606
48

Metode ekstensi berikut pada HttpRequest akan membuat string yang dapat disisipkan ke fiddler dan diputar ulang.

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;
            }
        }
    }
}
Sam Shiles
sumber
5
Kode yang sangat bagus! Tapi untuk ini bekerja dengan MVC 4 saya harus mengubah nama kelas untuk HttpRequestBaseExtensionsdan untuk mengubah HttpRequestke HttpRequestBasedi setiap tempat.
Dmitry
35

Anda dapat menggunakan variabel server ALL_RAW untuk mendapatkan tajuk HTTP asli yang dikirim bersama permintaan, maka Anda bisa mendapatkan InputStream seperti biasa:

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

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

Vincent de Lagabbe
sumber
Ini juga berhasil bagi saya. Bahkan tidak perlu menjadi pawang. Saya masih dapat mengaksesnya dari halaman.
Helephant
3
Atau dalam konteks server ASP.NET, gunakan: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar
Saya tidak bisa mendapatkan badan permintaan dari Request.InputStream, ia mengembalikan "" untuk saya setiap saat, namun ALL_RAW berfungsi dengan baik untuk mengembalikan header permintaan sehingga jawaban ini setengah benar.
Justin
1
Anda juga dapat menggunakan HttpContext.Current.Requestuntuk mengambil konteks saat ini di luar pengontrol MVC, halaman ASPX, dll ... pastikan saja itu bukan null pertama;)
jocull
16

Yah, saya sedang mengerjakan sebuah proyek dan melakukan, mungkin tidak terlalu dalam, sebuah log menggunakan params permintaan:

Lihatlah:

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

Anda dapat mendekorasi kelas pengontrol Anda untuk mencatatnya sepenuhnya:

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

atau catat hanya beberapa metode tindakan individu

[Log]
public ActionResult LoggedAction(){...}
John Prado
sumber
12

Adakah alasan Anda harus menyimpannya dalam kode terkelola?

Perlu disebutkan bahwa Anda dapat mengaktifkan Failed Trace logging di IIS7 jika Anda tidak suka menciptakan kembali roda. Ini mencatat tajuk, badan permintaan dan tanggapan, serta banyak hal lainnya.

Pencatatan Jejak yang Gagal

JoelBellot
sumber
Bagaimana jika itu bukan kegagalan?
Sinaesthetic
7
Anda dapat menggunakan Failed Trace logging dengan HTTP 200 OK juga, sehingga non-kegagalan masih dapat dicatat
JoelBellot
2
Sejauh ini, ini adalah solusi paling sederhana.
Kehlan Krumme
Untuk melihat jejak keseluruhan tumpukan, perlu ditambahkan GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;di akhir Register(..)in WebApiConfig.cs, tetapi ini dapat bervariasi antar versi.
Evgeni Sergeev
8

Saya pergi dengan pendekatan McKAMEY. Berikut adalah modul yang saya tulis yang akan membantu Anda memulai dan semoga menghemat waktu. Anda harus mencolokkan Logger dengan sesuatu yang cocok untuk Anda:

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
    }
}
GrokSrc
sumber
3
Anda tidak dapat dengan aman mengirimkan app.Response.Filter ke hal lain selain Stream. HttpModules lain mungkin membungkus filter respons Anda dengan milik mereka dan dalam hal ini Anda akan mendapatkan pengecualian pemeran yang tidak valid.
Micah Zoltu
Bukankah seharusnya begitu Encoding.UTF8, atau mungkin Encoding.Defaultsaat membaca aliran permintaan? Atau hanya menggunakan StreamReader( dengan membuang peringatan )
drzaus
5

OK, jadi sepertinya jawabannya adalah "tidak, Anda tidak bisa mendapatkan data mentah, Anda harus merekonstruksi permintaan / respons dari properti objek yang diuraikan". Oh well, saya sudah melakukan rekonstruksi.

Greg Beech
sumber
3
Apakah Anda melihat komentar Vineus tentang ServerVariables ["ALL_RAW"]? Saya belum mencobanya sendiri, tetapi didokumentasikan untuk mengembalikan informasi header mentah persis seperti yang dikirim oleh klien. Bahkan jika dokumen itu ternyata salah, dan ia sedang melakukan rekonstruksi, hei, rekonstruksi gratis :-)
Jonathan Gilbert
3

gunakan 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));
        }
    }
Figment Engine
sumber
3
Per Alex dan pengalaman saya sendiri, saya tidak berpikir Anda dapat membaca dari HttpResponse.OutputStream, jadi metode Anda masuk dalam metode ProcessResponse mungkin tidak akan berfungsi.
William Gross
2
William benar. HttpResponse.OutputStream tidak dapat dibaca. Saya menemukan solusi yang menggunakan HttpResponse.Filter dan mengganti aliran output default dengan Anda sendiri.
Eric Fan
endurasoft.com/blog/post/… async Httpmodule lebih baik?
PreguntonCojoneroCabrón
3

jika untuk penggunaan sesekali, untuk menyiasati, bagaimana dengan sesuatu yang kasar seperti di bawah ini?

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
Baburaj
sumber
1

Anda dapat melakukan ini dalam DelegatingHandlertanpa menggunakan yang OutputFilterdisebutkan dalam jawaban lain di .NET 4.5 menggunakan Stream.CopyToAsync()fungsi.

Saya tidak yakin tentang detailnya, tetapi itu tidak memicu semua hal buruk yang terjadi ketika Anda mencoba untuk langsung membaca aliran respons.

Contoh:

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.
    }
}
Talonj
sumber
0

Saya tahu ini bukan kode yang dikelola, tapi saya akan menyarankan filter ISAPI. Sudah beberapa tahun sejak saya memiliki "kesenangan" mempertahankan ISAPI saya sendiri tetapi dari apa yang saya ingat Anda bisa mendapatkan akses ke semua hal ini, baik sebelum dan sesudah ASP.Net telah melakukan hal itu.

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

Jika HTTPModule tidak cukup baik untuk apa yang Anda butuhkan, maka saya rasa tidak ada cara yang dikelola untuk melakukan ini dalam jumlah detail yang diperlukan. Ini akan menjadi sakit untuk dilakukan.

Chris
sumber
0

Saya setuju dengan yang lain, gunakan IHttpModule. Lihatlah jawaban untuk pertanyaan ini, yang melakukan hal yang hampir sama dengan yang Anda tanyakan. Ini mencatat permintaan dan respons, tetapi tanpa header.

Bagaimana cara melacak permintaan ScriptService WebService?

Jrummell
sumber
0

Mungkin yang terbaik untuk melakukan ini di luar aplikasi Anda. Anda dapat mengatur proxy terbalik untuk melakukan hal-hal seperti ini (dan banyak lagi). Proxy terbalik pada dasarnya adalah server web yang berada di ruang server Anda, dan berdiri di antara server web Anda dan klien. Lihat http://en.wikipedia.org/wiki/Reverse_proxy

Lance Fisher
sumber
0

Setuju dengan FigmentEngine, IHttpModuletampaknya menjadi cara untuk pergi.

Lihatlah ke dalam httpworkerrequest, readentitybodydan GetPreloadedEntityBody.

Untuk mendapatkan httpworkerrequestAnda perlu melakukan ini:

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

di mana objek inAppaplikasi http.

stevenrcfox
sumber
1
Saya sudah mengatakan bahwa jawaban itu tidak cocok karena tidak menangkap sebagian besar informasi yang saya minta. Bagaimana jawaban ini membantu?
Greg Beech
Lebih banyak menjelaskan, Bagaimana jawaban ini membantu?
PreguntonCojoneroCabrón
0

HttpRequestdan HttpResponsepra MVC dulu memiliki GetInputStream()dan GetOutputStream()yang dapat digunakan untuk tujuan itu. Belum melihat bagian-bagian dalam MVC jadi saya tidak yakin mereka tersedia tetapi mungkin ide :)

Rune FS
sumber