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

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

我不确定的是如何以最少“损坏”的方式获得这些数据。我可以通过检查 HttpRequest对象的所有属性并根据它们构建一个字符串(对于响应也是类似的)来重新构建我认为的请求,但是我真的希望获得通过网络发送的实际请求/响应数据。

我很乐意使用任何拦截机制,比如过滤器、模块等,并且解决方案可以特定于 IIS7。但是,我更喜欢仅将它保存在托管代码中。

有什么建议吗?

编辑: 我注意到 HttpRequest有一个 SaveAs方法,它可以将请求保存到磁盘上,但是这个方法使用不能公开访问的大量内部助手方法从内部状态重建请求(这就是为什么不允许保存到用户提供的流我不知道)。因此,看起来我不得不尽最大努力从对象中重建请求/响应文本..。

编辑2: 请注意,我说的是 完整请求,包括方法、路径、头文件等。目前的反应只看身体流,其中不包括这一信息。

编辑3: 这里没有人读问题吗?到目前为止已经有五个答案了,但是没有一个答案能够提示我们如何得到这个原始的在线请求。是的,我知道我可以从请求对象捕获输出流、标题、 URL 和所有这些东西。我在问题里已经说过了,看:

我可以通过检查 HttpRequest 对象的所有属性并从中构建一个字符串(对于响应也是如此)来重新构建我认为的请求,但是我真的很想获得通过网络发送的实际请求/响应数据。

如果您知道 完整原始数据(包括头、 url、 http 方法等)根本无法检索,那么了解这一点将非常有用。类似地,如果你知道如何获取原始格式的文件(是的,我仍然是指包含头文件、 url、 http 方法等)而不必重新构造它,这就是我要求的,那么这将是非常有用的。但是告诉我可以从 HttpRequest/HttpResponse对象重建它是没有用的。我知道。我已经说过了。


请注意: 在任何人开始说这是一个坏主意,或将限制可伸缩性等之前,我们还将在分布式环境中实现节流、顺序传递和反重播机制,因此无论如何都需要数据库日志记录。我不是在讨论这是否是一个好主意,我在寻找如何实现它。

105333 次浏览

HttpRequestHttpResponse前 MVC 曾经有一个 GetInputStream()GetOutputStream(),可以用于这个目的。还没有在 MVC 中研究这些部分,所以我不确定它们是否可用,但可能是一个想法:)

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

同意 FigmentEngine 的观点,IHttpModule似乎是一条出路。

看看 httpworkerrequestreadentitybodyGetPreloadedEntityBody

要获得 httpworkerrequest,你需要这样做:

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

其中 inApp是 httpapplication 对象。

我知道这不是托管代码,但我建议使用 ISAPI 过滤器。我已经有好几年没有“愉快地”维护我自己的 ISAPI 了,但据我回忆,在 ASP.Net 完成它的任务之前和之后,你都可以访问所有这些东西。

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

如果一个 HTTPModule 不足以满足您的需要,那么我就不认为有任何可管理的方法可以在所需的细节数量上做到这一点。不过会很痛苦。

我同意其他人的观点,使用 IHttpModule。看一下这个问题的答案,它的作用和你问的几乎一样。它记录请求和响应,但是没有头。

如何跟踪 ScriptServiceWebService 请求?

嗯,我正在做一个项目,并且做了一个日志,也许不太深入,使用请求参数:

看看吧:

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

一定要使用 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"

注意,无法读取响应流使您必须向 Output 流添加一个筛选器并捕获一个副本。

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

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

您可以使用 ALL _ RAW 服务器变量获得与请求一起发送的原始 HTTP 头,然后您可以像往常一样获得 InputStream:

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

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

最好在应用程序之外执行此操作。您可以设置一个反向代理来执行这样的操作(以及更多操作)。反向代理基本上是一个位于服务器机房的 Web 服务器,它位于 Web 服务器和客户端之间。参见 http://en.wikipedia.org/wiki/Reverse_proxy

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

我采用了 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
}
}

如果只是偶尔使用,为了避免走弯路,那么下面这些粗糙的东西怎么样?

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

为什么需要将其保存在托管代码中?

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

Failed Trace Logging

您可以在 DelegatingHandler中实现这一点,而不需要使用。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.
}
}