Build an ASP.NET HttpRequest Log Filter
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version
Peter Bromberg

I"ve found it can be very useful to be able to peer into the entire HTTP request stream when you are debugging an ASP.NET web project. This is especially true if the stream contains custom data such as specially concatenated form fields, string representations of Xml Documents, and the like. The code I present below was inspired by Mike Woodring's HttpLogFilter. I've rewritten it completely in VB.NET from the original C#, and I've additionally wired it up to log to a SQL Server table. I've found that splitting up your data and putting each "event" in a logging table in the database makes it much easier to read out, sort, query and so forth, rather than attempting to find your particular stuff out of some text file. However, I've left Woodring's orginal text file code intact, so you now have a choice- if the dbConnectionString <appSetting> element is filled in in the web.config, it will use the database instead.




If you look through the code below, almost all of it will be self-explanatory; don't forget this is an HttpModule, not just a class library, so it needs to be wired up in the web.config:

<httpModules>
<add name="RequestResponseLogger" type="LoggingFilter, LoggingFilter" />
</httpModules>
</system.web>

You can see also the call to the second class, EchoStream, which essentially "clones" the Request Stream so that we can manipulate it for logging purposes while passing it through the ASP.NET Http Pipeline for normal processing at the same time.

Finally the neat thing about this is that even when it's installed, you can turn it on and off by setting the <appSettings>
LoggingFilter.Enabled value to either "true" or "false"

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Specialized
Imports System.Configuration
Imports System.IO
Imports System.Text
Imports System.Web
Imports System.Data
Imports System.Data.SqlClient
Imports System.Diagnostics
Public Class LoggingFilter
    Implements IHttpModule
  Private httpApp As HttpApplication
  Private responseStream As EchoStream
  Private logFileName As String
  Private maxLogSize As Long
  Private dbConnString As String = String.Empty
  Private strServer As String = String.Empty
  Private strDatabase As String = String.Empty
  Private strUser As String = String.Empty
  Private strPassword As String = String.Empty
    Public Overridable Sub Init(ByVal httpApp As HttpApplication) Implements IHttpModule.Init
        Me.httpApp = httpApp
        Dim flag As Boolean
        GetSettings(flag, logFileName, maxLogSize)
        If Not flag Then
            Return
        End If
        If logFileName Is Nothing Then
            Return
        End If
        AddHandler httpApp.BeginRequest, AddressOf Me.OnBeginRequest
        AddHandler httpApp.EndRequest, AddressOf Me.OnEndRequest
    End Sub
    Private Sub GetSettings(ByRef filterEnabled As Boolean, ByRef logFileName As String,  _
ByRef maxLogSize As Long) Dim i As Integer Dim flag As Boolean = False filterEnabled = 0 logFileName = Nothing maxLogSize = 0 Dim enabledFlagFound As Boolean = False Dim key As String Dim configKeys As String() = ConfigurationSettings.AppSettings.AllKeys For Each key In configKeys Select Case key Case "LoggingFilter.LogFile" logFileName = ConfigurationSettings.AppSettings("LoggingFilter.LogFile") Case "LoggingFilter.Enabled" filterEnabled = IIf(ConfigurationSettings.AppSettings("LoggingFilter.Enabled") = "true", _ True, False) enabledFlagFound = True Case "LoggingFilter.ResetLogSize" maxLogSize = Long.Parse(ConfigurationSettings.AppSettings("LoggingFilter.ResetLogSize")) Case "LoggingFilter.dbConnstring" Dim strText As String = ConfigurationSettings.AppSettings("LoggingFilter.dbConnstring") If strText.Length > 0 Then Dim strParms As String() = strText.Split(Convert.ToChar("|")) strServer = strParms(0) strDatabase = strParms(1) strUser = strParms(2) strPassword = strParms(3) dbConnString = "server=" & strServer & ";Database=" & strDatabase _ & ";User Id=" & strUser & ";password=" & strPassword End If End Select Next ' If the LoggingFilter.Enabled key was set in the config file, ' then just leave filterEnabled as it was configured. If the ' LoggingFilter.Enabled key wasn't in the config file at all, ' the default behavior will be to enable the filter as long as ' a file name was specified. The dbConnectionString key overrides files ' to write records to the database table. If Not enabledFlagFound Then filterEnabled = (logFileName <> Nothing) End If End Sub Private Sub OnBeginRequest(ByVal sender As Object, ByVal a As EventArgs) Dim flag As Boolean = False If maxLogSize > CLng(0) AndAlso File.Exists(logFileName) Then flag = New FileInfo(logFileName).Length < maxLogSize = False End If Dim stream As stream = File.Open(logFileName, _
IIf(Not flag, FileMode.Append, FileMode.Truncate), FileAccess.Write) LogRequestInfo(stream) responseStream = New EchoStream(httpApp.Response.Filter, stream) httpApp.Response.Filter = responseStream End Sub Private Sub OnEndRequest(ByVal sender As Object, ByVal a As EventArgs) If Not responseStream Is Nothing Then responseStream.Close() End If End Sub Private Sub LogRequestInfo(ByVal logFile As Stream) If dbConnString.Length < 2 Then Dim locals As Object() = New Object() {DateTime.Now} WriteLine(logFile, Chr(10) & Chr(10) & "Request: {0}", locals) WriteLine(logFile, httpApp.Request.Url.ToString(), New Object(-1) {}) locals = New Object() {httpApp.Request.HttpMethod, httpApp.Request.ContentType, _
httpApp.Request.ContentLength, httpApp.Request.ContentEncoding.EncodingName} WriteLine(logFile, "{0}, type: {1}, length: {2}, encoding: {3}", locals) WriteLine(logFile, "Request Content:", New Object(-1) {}) Dim l As Long = httpApp.Request.InputStream.Position CopyStream(httpApp.Request.InputStream, logFile) httpApp.Request.InputStream.Position = l WriteLine(logFile, Chr(10) & "Response:", New Object(-1) {}) Else Dim strSql As String = "INSERT INTO HTTPLOGITEMS " strSql += "( LogDateTime,RequestUrl,RequestMethod,RequestContentType," strSql += "RequestContentLength,RequestContentEncoding,RequestContentBody ) " strSql += " VALUES " strSql += "('" strSql += DateTime.Now.ToString() strSql += "','" strSql += httpApp.Request.Url.ToString() strSql += "','" strSql += httpApp.Request.HttpMethod strSql += "','" strSql += httpApp.Request.ContentType strSql += "'," strSql += httpApp.Request.ContentLength.ToString() strSql += ",'" strSql += httpApp.Request.ContentEncoding.EncodingName strSql += "','" ' Dim ms As New MemoryStream() Dim l As Long = httpApp.Request.InputStream.Position Dim str As Stream = httpApp.Request.InputStream Dim strLen As Integer = CInt(str.Length) Dim strArr(strLen) As Byte Dim strRead As Integer = str.Read(strArr, 0, strLen) httpApp.Request.InputStream.Position = l Dim strContent As String strContent = System.Text.Encoding.UTF8.GetString(strArr) strSql += strContent strSql += "')" Dim cn1 As New SqlConnection(dbConnString) cn1.Open() Dim cmd As New SqlCommand() cmd.Connection = cn1 cmd.CommandText = strSql Dim res As Integer = cmd.ExecuteNonQuery() End If End Sub Private Sub WriteLine(ByVal s As Stream, ByVal format As String, _ ByVal ParamArray args As Object()) If dbConnString.Length < 2 Then Dim str As String = String.Format(String.Concat(format, Chr(10)), args) Dim bs As Byte() = Encoding.ASCII.GetBytes(str) s.Write(bs, 0, CInt(bs.Length)) End If End Sub Private Sub CopyStream(ByVal inStream As Stream, ByVal outStream As Stream) Dim bs As Byte() = New Byte(127) {} Dim i As Integer = 0 While (i = inStream.Read(bs, 0, CInt(bs.Length))) > 0 outStream.Write(bs, 0, i) End While End Sub Public Overridable Sub Dispose() Implements IHttpModule.Dispose End Sub End Class

Imports Microsoft.VisualBasic
Imports System
Imports System.IO
Public Class EchoStream
    Inherits Stream
    Private primaryStream As Stream
    Private otherStream As Stream
    Public Overrides ReadOnly Property CanRead() As Boolean
        Get
            Return primaryStream.CanRead
        End Get
    End Property
    Public Overrides ReadOnly Property CanSeek() As Boolean
        Get
            Return primaryStream.CanSeek
        End Get
    End Property
    Public Overrides ReadOnly Property CanWrite() As Boolean
        Get
            Return primaryStream.CanWrite
        End Get
    End Property
    Public Overrides ReadOnly Property Length() As Long
        Get
            Return primaryStream.Length
        End Get
    End Property
    Public Overrides Property Position() As Long
        Get
            Return primaryStream.Position
        End Get
        Set(ByVal value As Long)
            Dim l As Long
            otherStream.Position = l = value
            primaryStream.Position = l
        End Set
    End Property
    Public Sub New(ByVal primaryStream As Stream, ByVal otherStream As Stream)
        Me.primaryStream = primaryStream
        Me.otherStream = otherStream
    End Sub
    Public Overrides Function Seek(ByVal offset As Long, ByVal direction As SeekOrigin) As Long
        Dim l As Long = primaryStream.Seek(offset, direction)
        If otherStream.Seek(offset, direction) <> l Then
            Throw New ApplicationException("Failed to keep streams in sync while seeking.")
        Else
            Return l
        End If
    End Function
    Public Overrides Sub SetLength(ByVal length As Long)
        primaryStream.SetLength(length)
        otherStream.SetLength(length)
    End Sub
    Public Overrides Sub Close()
        primaryStream.Close()
        otherStream.Close()
    End Sub
    Public Overrides Sub Flush()
        primaryStream.Flush()
        otherStream.Flush()
    End Sub
    Public Overrides Function Read(ByVal buffer As Byte(), ByVal offset As Integer, _
ByVal count As Integer) As Integer Dim i As Integer = primaryStream.Read(buffer, offset, count) otherStream.Position = primaryStream.Position Return i End Function Public Overrides Sub Write(ByVal buffer As Byte(), _ ByVal offset As Integer, ByVal count As Integer) primaryStream.Write(buffer, offset, count) otherStream.Write(buffer, offset, count) End Sub Public Overrides Function ReadByte() As Integer Dim i As Integer = primaryStream.ReadByte() otherStream.Position = primaryStream.Position Return i End Function Public Overrides Sub WriteByte(ByVal b As Byte) primaryStream.WriteByte(b) otherStream.WriteByte(b) End Sub End Class

The SQL Script to create the database and the HttpLogItems table is in the downloadable VS.NET 2003 solution, along with a sample web app that tests it and even displays the log results in a DataGrid. The only thing you may need to do is add a default value of newid() to the LogID column in the table, and, of course, change the connection string to suit your web machine. Note that the

<add key="LoggingFilter.dbConnstring" value="(local)|httplog|sa|" />

element is Pipe delimited - Server, database, username, password. Enjoy!

Download the code accompanying this article

 


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.