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

153

我正在编写一个 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
可能可以使用IIS7和本地模块来实现 - http://msdn.microsoft.com/en-us/library/ms694280.aspx - Daniel Crenna
你已经实现了吗?只是好奇,你是否采用了任何缓冲策略来写入数据库? - systempuntoout
1
有趣的项目...如果你最终完成了它,会有什么样的解决方案? - PreguntonCojoneroCabrón
15个回答

94

一定要使用 IHttpModule 并实现 BeginRequestEndRequest 事件。

所有的“原始”数据都存在于 HttpRequestHttpResponse 之间,只是它们不是以单个原始格式存在。以下是构建类似 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;

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

接下来,使用装饰器模式实现 OutputFilterStream 作为流的包装器:

/// <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中实现,但如果我运行ToString,我只会得到类名作为字符串。.ReadStream返回所需的响应正文。) - Adam
2
我认为Mckamey是个天才。你能去微软工作,这样我们就可以得到智能解决方案,而不需要聪明的变通方法吗? - Abacus
4
请注意,request.RawUrl可能会触发请求验证异常。在4.5中,您可以使用request.Unvalidated.RawUrl来防止此问题。在4.0中,我最终使用了一些反射来模拟Request.SaveAs。 - Freek
我遇到了类似于读取请求时读取响应的问题 - 使用StreamReader读取后,request.InputStream.Length为0。在阅读了这个参考之后,发现这只是一个愚蠢的陷阱。 - drzaus
1
@mckamey 我尝试在我的 global.asax 中使用 Application_BeginRequest 和 Application_EndRequest 实现您的解决方案,但我不确定在 EndRequest 中应该编写什么代码,请您在回答中提供一个示例好吗? - Jerome2606
显示剩余4条评论

52
HttpRequest的以下扩展方法将创建一个字符串,可以复制到Fiddler中以进行回放。
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;
            }
        }
    }
}

6
非常好的代码!但是为了使其与MVC 4兼容,我不得不将类名更改为HttpRequestBaseExtensions,并在每个地方将HttpRequest更改为HttpRequestBase - Dmitry

35

这对我也起作用了。甚至不需要在处理程序中。我仍然可以从页面访问它。 - Helephant
3
在ASP.NET服务器上下文中,使用以下代码:this.Request.ServerVariables["ALL_RAW"]; - Peter Stegnar
我无法从Request.InputStream中获取请求正文,每次它都会返回"",但是ALL_RAW非常适合返回请求标头,因此这个答案是半对的。 - Justin
1
你也可以使用 HttpContext.Current.Request 在 MVC 控制器、ASPX 页面等之外获取当前上下文……只要确保它不是 null 就行了 ;) - jocull

17

嗯,我正在做一个项目,并使用请求参数记录了一些日志,可能不算太深入。

看一下:

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(){...}

15

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

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

Failed Trace Logging


如果这不是一个失败呢? - Sinaesthetic
7
你可以将“失败追踪记录”应用在HTTP 200 OK请求中,这样即便是非失败的操作也可以被记录下来。 - JoelBellot
2
这绝对是最简单的解决方案。 - keeehlan
为了查看整个堆栈跟踪,需要在WebApiConfig.csRegister(..)末尾添加GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;,但这可能因版本而异。 - Evgeni Sergeev

10

我采用了McKAMEY的方法。这里是我写的一个模块,它可以让你开始并希望节省一些时间。你需要使用适合你的内容来连接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
    }
}

4
除了流(Stream)之外,您不能安全地将 app.Response.Filter 强制转换为任何其他内容。其他 HttpModules 可能会使用自己的响应过滤器来包装您的过滤器,在这种情况下,将会出现无效转换异常。 - Micah Zoltu
读取请求流时,难道不应该使用 Encoding.UTF8 或者 Encoding.Default 吗?或者只需使用 StreamReader(请注意释放的问题)。 - drzaus

6

好的,看起来答案是“不,你不能获取原始数据,必须从解析对象的属性重新构建请求/响应”。噢,那我已经做了重构的事情。


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

4
根据Alex和我的经验,我认为你无法从HttpResponse.OutputStream中读取数据,因此你在ProcessResponse方法中记录日志的方式可能不会起作用。 - William Gross
2
William是正确的。HttpResponse.OutputStream不可读。我找到了一个解决方案,就是使用HttpResponse.Filter并用自己的输出流替换默认输出流。 - Eric Fan
实现异步 Httpmodule 更好吗? - PreguntonCojoneroCabrón

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
您可以在使用Stream.CopyToAsync()函数的.NET 4.5中,通过DelegatingHandler来完成此操作,而无需使用其他答案中提到的OutputFilter。我不确定细节,但它不会触发直接读取响应流时发生的所有糟糕事情。
示例:
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.
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接