Logging TraceListener
I’m working on a console app that needs to provide integrated logging of its own output. Sure, you could do a standard console output redirect, but I wanted the app to be responsible for logging its own output. I decided to write my own TraceListener
that automatically creates IIS-style cyclic logfiles using the Trace
method, like so:
Sub Main() AddListeners(False) Trace.WriteLine("Hello World!") Trace.WriteLine("Hello World!", "category1") Dim h As New Hashtable Trace.WriteLine(h) Trace.WriteLine(h, "category2") For i As Integer = 0 To 99 Trace.WriteLine("Line " & i) Next End Sub Private Sub AddListeners(ByVal DoLog As Boolean) '-- this causes Trace.Write to '-- mimic Console.Write Dim t As New TextWriterTraceListener(System.Console.Out) Trace.Listeners.Add(t) '-- this enables IIS-style logging If DoLog Then Dim ct As New CyclicLogTraceListener ct.FolderName = "." ct.FileCountThreshold = 3 ct.FileSizeThreshold = 3500 ct.FileSizeUnit = CyclicLogTraceListener.SizeUnit.Bytes ct.FileNameTemplate = "{0:0000}.log" ct.TimeStampFormat = "yyyy-dd-MM hh:mm:ss" ct.AddMethod = True ct.AddPidTid = True ct.FieldSeparator = ", " Trace.Listeners.Add(ct) End If End Sub
You can either add the listener in code, as above, or more dynamically via the System.Diagnostics
section of the .config file:
<system.diagnostics> <trace autoflush="true" indentsize="4"> <listeners> <add name="CyclicLog" type="ConsoleApp.CyclicLogTraceListener,ConsoleApp" initializeData="fileSizeThreshold=5000, fileCountThreshold=3, addPidTid=True" /> </listeners> </trace> </system.diagnostics>
This results in a log file named 0000.log in the application folder that looks like so:
2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Hello World! 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, category1, Hello World! 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, System.Collections.Hashtable 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, category2, System.Collections.Hashtable 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Line 0 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Line 1 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Line 2 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Line 3 2005-07-03 12:25:43, 1392/1476, ConsoleApp.Module1.Main, Line 4
The neat thing is that we get this behavior for free – as long as I use Trace.WriteLine
instead of Console.WriteLine
, my console app logs its own output, and I can easily modify the logging behavior post-deployment by editing the .config file.
Code follows...
Here’s the complete CyclicLogTraceListener class:
<Imports System> <Imports System.Diagnostics> <Imports System.IO> <Imports System.Reflection> <Imports System.Text> <Imports System.Text.RegularExpressions> <Public Class CyclicLogTraceListener> <Inherits TraceListener> <Private Const _StackFrameSkipCount As Integer=5> <Private Const _IndentCharacter As Char=" "c> <Private _FileIndex As Long=0> <Private _FirstLogFound As Boolean=False> <Private _FileNameTemplateHasFormatting As Boolean=False> <Private _FileLength As Long=0> <Private _FileCreationDate As DateTime=DateTime.MinValue> <Private _sw As StreamWriter> <#Region "Properties"> <Private _FolderName As String> <Private _FieldSeparator As String> <Private _FileSizeThreshold As Long> <Private _FileSizeUnit As SizeUnit> <Private _FileCountThreshold As Long> <Private _FileName As String> <Private _FileNameTemplate As String> <Private _TimeStampFormat As String> <Private _AddMethod As Boolean> <Private _AddPidTid As Boolean> <Private _AutoFlush As Boolean> <Private _FileAgeThreshold As Long> <Private _FileAgeUnit As AgeUnit> <!-- Enums --> <Public Enum AgeUnit> <Minutes> <Hours> <Days> <Weeks> <Months> <End Enum> <Public Enum SizeUnit> <Gigabytes> <Megabytes> <Kilobytes> <Bytes> <End Enum> <!-- Properties --> <Public Property AutoFlush() As Boolean> <Get><Return _AutoFlush><End Get> <Set(ByVal Value As Boolean)><_AutoFlush=Value><End Set> <End Property> <Public Property FolderName() As String> <Set(ByVal Value As String)> <_FolderName=Value> <If Not _FolderName.EndsWith(Path.DirectorySeparatorChar) Then> <_FolderName=_FolderName & Path.DirectorySeparatorChar> <End If> <If Not Directory.Exists(_FolderName) Then> <Throw New DirectoryNotFoundException("Requested trace logging directory '" & _FolderName & "' does not exist")> <End If> <End Set> <Get><Return _FolderName><End Get> <End Property> <Public Property FieldSeparator() As String> <Set(ByVal Value As String)><_FieldSeparator=Value><End Set> <Get><Return _FieldSeparator><End Get> <End Property> <#End Region> <#Region "Public Methods"> <Public Sub New()> <Me.FileNameTemplate="{0:0000}.log"> <_FolderName="."> <_FileSizeThreshold=1> <_FileSizeUnit=SizeUnit.Megabytes> <_FileCountThreshold=10000> <_TimeStampFormat="yyyy-dd-MM hh:mm:ss"> <_AddMethod=False> <_AddPidTid=False> <_FieldSeparator=", "> <_FileAgeUnit=AgeUnit.Days> <_FileAgeThreshold=0> <_AutoFlush=True> <End Sub> <Public Sub New(ByVal initializeData As String)> <Me.New()> <FolderName=ParseString(initializeData, "folderName", _FolderName)> <_FileSizeThreshold=ParseLong(initializeData, "fileSizeThreshold", _FileSizeThreshold)> <_FileSizeUnit=DirectCast(ParseEnum(initializeData, "fileSizeUnit", _FileSizeUnit, GetType(SizeUnit)), SizeUnit)> <_FileCountThreshold=ParseLong(initializeData, "fileCountThreshold", _FileCountThreshold)> <_FileAgeThreshold=ParseLong(initializeData, "fileAgeThreshold", _FileAgeThreshold)> <_FileAgeUnit=DirectCast(ParseEnum(initializeData, "fileAgeUnit", _FileAgeUnit, GetType(AgeUnit)), AgeUnit)> <_FileNameTemplate=ParseString(initializeData, "fileNameTemplate", _FileNameTemplate)> <_TimeStampFormat=ParseString(initializeData, "timeStampFormat", _TimeStampFormat)> <_AddPidTid=ParseBoolean(initializeData, "addPidTid", _AddPidTid)> <_AddMethod=ParseBoolean(initializeData, "addMethod", _AddMethod)> <_FieldSeparator=ParseString(initializeData, "fieldSeparator", _FieldSeparator)> <End Sub> <Public Overloads Overrides Sub Write(ByVal message As String)> <WriteMessage(FormatMessage(message, "", False))> <End Sub> <Public Overloads Overrides Sub WriteLine(ByVal message As String)> <WriteMessage(FormatMessage(message, "", True))> <End Sub> <Public Overrides Sub Close()> <SyncLock Me> <CloseLogFile()> <End SyncLock> <End Sub> <Public Overrides Sub Flush()> <SyncLock Me> <If Not _sw Is Nothing Then> <_sw.Flush()> <End If> <End SyncLock> <End Sub> <#End Region> <#End Class>