在ASP.NET MVC和IIS7中记录原始HTTP请求/响应


140

我正在编写一个Web服务(使用ASP.NET MVC),出于支持目的,我们希望能够以与原始在线格式(即包括HTTP)尽可能接近的方式记录请求和响应方法,路径,所有标头和正文)放入数据库。

我不确定如何以最少的“混杂”方式获取此数据。我可以通过检查HttpRequest对象的所有属性并从中构造一个字符串(并类似地用于响应)来重新构造我认为请求的外观,但我真的很想掌握实际的请求/响应数据电线上发送。

我很高兴使用任何拦截机制,例如过滤器,模块等,并且该解决方案可以特定于IIS7。但是,我宁愿仅将其保留在托管代码中。

有什么建议吗?

编辑:我注意到,HttpRequest有一种SaveAs方法可以将请求保存到磁盘,但是可以使用内部无法公开访问的内部帮助程序方法从内部状态重构请求(相当多的原因这不允许保存到用户提供的方法流,我不知道)。因此,开始看起来我必须尽力从对象中重建请求/响应文本……吟。

编辑2:请注意,我说了整个请求,包括方法,路径,标头等。当前响应仅查看不包含此信息的主体流。

编辑3:这里没有人阅读问题吗?到目前为止,有五个答案,但甚至没有人暗示一种获取整个原始在线请求的方法。是的,我知道我可以从请求对象中捕获输出流,标头,URL和所有其他内容。我已经在问题中说过,请参阅:

我可以通过检查HttpRequest对象的所有属性并从中构建一个字符串(以及类似的响应)来重新构造我认为的请求外观,但是我真的很想掌握实际的请求/响应数据是通过网络发送的。

如果您知道完全无法检索完整的原始数据(包括标题,URL,http方法等),那么这将很有用。同样,如果您知道如何以原始格式获取所有内容(是的,我仍然表示包括标头,url,http方法等),而无需重构(这就是我的要求),那么这将非常有用。但是告诉我可以从HttpRequest/ HttpResponse对象重建它没有用。我知道。我已经说过了


请注意:在任何人开始说这不是一个好主意,或将限制可伸缩性等之前,我们还将在分布式环境中实现限制,顺序交付和抗重播机制,因此无论如何都需要数据库日志记录。我不是在讨论关于这是否是一个好主意,我正在寻找如何做到这一点。


1
@Kev-不,这是使用ASP.NET MVC实现的RESTful服务
Greg Beech,2009年


您设法实现了吗?只是好奇,您是否采用了任何缓冲策略来写入db?
systempuntoout 2015年

1
有趣的项目...如果您最终做得到最终解决方案?
PreguntonCojoneroCabrón

Answers:


91

绝对使用IHttpModule和实现BeginRequestand EndRequest事件。

所有的“原始”数据都存在于HttpRequest和之间HttpResponse,而并不是原始格式。这是构建Fiddler样式的转储所需的部分(大约与原始HTTP差不多):

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

对于响应:

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

请注意,您无法读取响应流,因此必须向输出流添加过滤器并捕获副本。

在中BeginRequest,您需要添加响应过滤器:

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

filterEndRequest处理程序中存储可以访问它的位置。我建议HttpContext.Items。然后可以在中获取完整的响应数据filter.ReadStream()

然后OutputFilterStream使用Decorator模式作为流的包装器来实现:

/// <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
好答案。不过有一条评论:您说“然后可以在filter.ToString()中获取完整的响应数据。” -您的意思不是filter.ReadStream()吗?(我在vb.net中实现的不是c#,但如果我运行ToString,我只是将类名称作为字符串获取。.ReadStream返回所需的响应正文。–
Adam

我同意,很好的答案。我已将其用作自定义记录器的基础,但现在遇到了一些标头丢失的问题,最重要的是,在使用IIS压缩时,我无法访问最终的压缩响应。为此,我提出了一个新的相关问题(stackoverflow.com/questions/11084459/…)。
克里斯(Chris

2
我认为麦凯米是个天才。您是否可以为Microsoft工作,以便我们仅获得智能解决方案,而无需采用出色的解决方法?
算盘

4
当心request.RawUrl可能会触发请求验证异常。在4.5中,您可以使用request.Unvalidated.RawUrl来防止这种情况。在4.0中,我最终使用了一些模仿来模仿Request.SaveAs
Freek

1
@mckamey我尝试使用Application_BeginRequest和Application_EndRequest在global.asax中实现您的解决方案,但是我不确定应该在EndRequest中编写什么代码,您能否在答案plz中提供示例?
Jerome2606 '18

48

HttpRequest上的以下扩展方法将创建一个字符串,可以将该字符串粘贴到提琴手中并进行重播。

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
很好的代码!但是,要使其与MVC 4配合使用,我必须在每个位置将类名HttpRequestBaseExtensions更改HttpRequestHttpRequestBase,并更改为。
德米特里(Dmitry)

35

您可以使用ALL_RAW服务器变量获取与请求一起发送的原始HTTP标头,然后可以照常获取InputStream:

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

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


这也对我有用。甚至不需要在处理程序中。我仍然可以从页面访问它。
Helephant,2011年

3
或在ASP.NET服务器上下文中,使用:this.Request.ServerVariables [“ ALL_RAW”];
Peter Stegnar

我无法从Request.InputStream获取请求主体,它每次都会为我返回“”,但是ALL_RAW对于返回请求标头的效果很好,因此此答案是正确的一半。
贾斯汀

1
您还可以使用HttpContext.Current.RequestMVC控制器,ASPX页面等之外的其他内容来获取当前上下文...只需确保它不首先为null即可;)
jocull

16

好吧,我正在研究一个项目,并使用请求参数做了一个日志,也许不太深:

看一看:

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

您可以装饰控制器类以完全记录它:

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

或仅记录一些单独的操作方法

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

12

有什么理由需要将其保留在托管代码中?

值得一提的是,如果您不想重新发明轮子,可以在IIS7中启用“ 失败跟踪”日志记录。这将记录标头,请求和响应正文以及许多其他内容。

跟踪记录失败


如果不是失败怎么办?
Sinaesthetic,2015年

7
您也可以使用HTTP 200 OK进行失败跟踪日志记录,因此仍然可以记录未失败消息
JoelBellot 2015年

2
到目前为止,这是最简单的解决方案。
Kehlan Krumme '16

要查看整个堆栈跟踪,有必要GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;Register(..)in 的末尾添加WebApiConfig.cs,但这在版本之间可能有所不同。
Evgeni Sergeev

8

我接受了麦卡梅的做法。这是我写的一个模块,可以帮助您入门,并希望为您节省一些时间。您显然需要使用适合您的东西来插入Logger:

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
您不能安全地将app.Response.Filter强制转换为Stream以外的任何内容。其他HttpModule可能会将响应过滤器包装在一起,在这种情况下,您将获得无效的强制转换异常。
米卡·佐尔图

是不是Encoding.UTF8,或者Encoding.Default在阅读请求流时?或只是使用StreamReader带有警告
drzaus

5

好的,所以看起来答案是“不,您无法获取原始数据,您必须从已解析对象的属性中重建请求/响应”。哦,好,我已经完成了重建工作。


3
您是否看到Vineus对ServerVariables [“ ALL_RAW”]的评论?我还没有亲自尝试过,但是据记载,它可以返回客户端所发送的原始标头信息。即使该文件被证明是错误的,它正在进行重建,嘿,自由重建:-)
Jonathan Gilbert

3

使用一个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
根据Alex和我的经验,我认为您无法从HttpResponse.OutputStream中读取内容,因此您登录ProcessResponse方法的方法可能无法正常工作。
威廉·格罗斯

2
威廉是对的。HttpResponse.OutputStream不可读。我找到了一种解决方案,该解决方案是使用HttpResponse.Filter并将默认的输出流替换为您自己的。
埃里克·范


3

如果偶尔使用,为了避开弯角,下面的东西如何呢?

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

您可以在DelegatingHandler不使用OutputFilter.NET 4.5中使用该Stream.CopyToAsync()功能的其他答案中提及的情况下完成此任务。

我不确定具体细节,但是当您尝试直接读取响应流时,它不会触发所有发生的坏事情。

例:

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

我知道这不是托管代码,但我将建议使用ISAPI过滤器。自从拥有维护自己的ISAPI的“乐趣”已经过去了两年,但是从我的回忆中,您可以在ASP.Net完成之前和之后都可以访问所有这些东西。

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

如果HTTPModule不能满足您的需要,那么我只是认为没有任何托管的方式可以做到所需的详细信息。不过这会很痛苦。




0

同意FigmentEngine,这IHttpModule似乎是可行的方法。

看看httpworkerrequestreadentitybodyGetPreloadedEntityBody

要获得httpworkerrequest您需要执行以下操作:

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

inApphttpapplication对象在哪里。


1
我已经说过这个答案不合适,因为它不能捕获我所要求的大多数信息。这个答案对您有什么帮助?
格雷格·比奇

更多说明,此答案有何帮助?
PreguntonCojoneroCabrón

0

HttpRequestHttpResponsepre MVC曾经有一个GetInputStream()GetOutputStream()可用于该目的。尚未研究MVC中的那些部分,所以我不确定它们是否可用,但可能是一个主意:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.