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>