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>

Related posts

The Problem With Logging

A recent Stack Overflow post described one programmer's logging style. Here's what he logs: INFO Level * The start and end of the method * The start and end of any major loops * The start of any major case/switch statements DEBUG Level * Any parameters passed into the

By Jeff Atwood ·
Comments

Recent Posts

Stay Gold, America

Stay Gold, America

We are at an unprecedented point in American history, and I'm concerned we may lose sight of the American Dream.

By Jeff Atwood ·
Comments
The Great Filter Comes For Us All

The Great Filter Comes For Us All

With a 13 billion year head start on evolution, why haven’t any other forms of life in the universe contacted us by now? (Arrival is a fantastic movie. Watch it, but don’t stop there – read the Story of Your Life novella it was based on for so much

By Jeff Atwood ·
Comments
I Fight For The Users

I Fight For The Users

If you haven’t been able to keep up with my blistering pace of one blog post per year, I don’t blame you. There’s a lot going on right now. It’s a busy time. But let’s pause and take a moment to celebrate that Elon Musk

By Jeff Atwood ·
Comments
The 2030 Self-Driving Car Bet

The 2030 Self-Driving Car Bet

It’s my honor to announce that John Carmack and I have initiated a friendly bet of $10,000* to the 501(c)(3) charity of the winner’s choice: By January 1st, 2030, completely autonomous self-driving cars meeting SAE J3016 level 5 will be commercially available for passenger use

By Jeff Atwood ·
Comments