I <3 Steve McConnell*
Coding Horror
programming and human factors
by Jeff Atwood

March 06, 2005

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

    ''' <summary>
    ''' Indicates what unit of time FileAgeThreshold represents
    ''' </summary>
    Public Enum AgeUnit
        Minutes
        Hours
        Days
        Weeks
        Months
    End Enum

    ''' <summary>
    ''' Indicates what unit of size FileBytesThreshold represents
    ''' </summary>
    Public Enum SizeUnit
        Gigabytes
        Megabytes
        Kilobytes
        Bytes
    End Enum


    ''' <summary>
    ''' If true, log file is flushed after every write.
    ''' Can also be set via trace="autoflush" in the 
    ''' system.diagnostics .config file section
    ''' </summary>
    Public Property AutoFlush() As Boolean
        Get
            Return _AutoFlush
        End Get
        Set(ByVal Value As Boolean)
            _AutoFlush = Value
        End Set
    End Property

    ''' <summary>
    ''' Folder that log files will be written to.
    ''' Defaults to current folder.
    ''' </summary>
    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

    ''' <summary>
    ''' Seperator used between log field entries.
    ''' Defaults to comma.
    ''' </summary>
    Public Property FieldSeparator() As String
        Set(ByVal Value As String)
            _FieldSeparator = Value
        End Set
        Get
            Return _FieldSeparator
        End Get
    End Property

    ''' <summary>
    ''' Template used to generate log filenames
    ''' supports standard String.Format for two values: file index {0:} and current date {1:}
    ''' using the standard String.Format conventions
    ''' Defaults to "{0:0000}.log"
    ''' </summary>
    Public Property FileNameTemplate() As String
        Set(ByVal Value As String)
            _FileNameTemplate = Value
            _FileNameTemplateHasFormatting = Regex.IsMatch(_FileNameTemplate, "{(0|1):.*}")
        End Set
        Get
            Return _FileNameTemplate
        End Get
    End Property

    ''' <summary>
    ''' Add the method name of the calling function to the log.
    ''' Defaults to True.
    ''' </summary>
    Public Property AddMethod() As Boolean
        Set(ByVal Value As Boolean)
            _AddMethod = Value
        End Set
        Get
            Return _AddMethod
        End Get
    End Property

    ''' <summary>
    ''' Add the process and thread ID to the log.
    ''' Defaults to False.
    ''' </summary>
    Public Property AddPidTid() As Boolean
        Set(ByVal Value As Boolean)
            _AddPidTid = Value
        End Set
        Get
            Return _AddPidTid
        End Get
    End Property

    ''' <summary>
    ''' If a format string is provided, the time will be added to each log entry.
    ''' Defaults to "yyyy-MM-dd hh:mm:ss". Set to empty string to disable.
    ''' </summary>
    Public Property TimeStampFormat() As String
        Set(ByVal Value As String)
            _TimeStampFormat = Value
        End Set
        Get
            Return _TimeStampFormat
        End Get
    End Property

    ''' <summary>
    ''' Maximum number of log files to create.
    ''' Defaults to 10000.
    ''' </summary>
    Public Property FileCountThreshold() As Long
        Set(ByVal Value As Long)
            _FileCountThreshold = Value
        End Set
        Get
            Return _FileCountThreshold
        End Get
    End Property

    ''' <summary>
    ''' Maximum age, in FileAgeUnits, of log files before a new log file will be created
    ''' Defaults to 0, infinite
    ''' </summary>
    Public Property FileAgeThreshold() As Long
        Get
            Return _FileAgeThreshold
        End Get
        Set(ByVal Value As Long)
            _FileAgeThreshold = Value
        End Set
    End Property

    ''' <summary>
    ''' Determines what time unit is represented in FileAgeThreshold. 
    ''' Defaults to AgeUnit.Days
    ''' </summary>
    Public Property FileAgeUnit() As AgeUnit
        Get
            Return _FileAgeUnit
        End Get
        Set(ByVal Value As AgeUnit)
            _FileAgeUnit = Value
        End Set
    End Property

    ''' <summary>
    ''' Maximum file size each log is allowed to grow to before a new log is created. 
    ''' Defaults to 512kb.
    ''' </summary>
    Public Property FileSizeThreshold() As Long
        Set(ByVal Value As Long)
            _FileSizeThreshold = Value
        End Set
        Get
            Return (_FileSizeThreshold)
        End Get
    End Property

    ''' <summary>
    ''' Determines what size unit is represented in FileSizeThreshold. 
    ''' Defaults to Bytes.
    ''' </summary>
    Public Property FileSizeUnit() As SizeUnit
        Get
            Return _FileSizeUnit
        End Get
        Set(ByVal Value As SizeUnit)
            _FileSizeUnit = Value
        End Set
    End Property

#End Region

#Region "  Public Methods"

    ''' <summary>
    ''' constructor contains defaults if values aren't specified
    ''' </summary>
    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

    ''' <summary>
    ''' this method is used when trace configured via the system.diagnostics section of the .config file
    ''' all the parameters are set via a single initializeData string in this format:
    '''   "booleanValue=true, stringValue='string', longValue=567"
    ''' </summary>
    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

#Region "  Initialization Parsing"

    Private Function ParseEnum(ByVal initializeData As String, ByVal name As String, _
        ByVal defaultValue As Object, ByVal t As Type) As Object
        Dim s As String = ParseString(initializeData, name, defaultValue.ToString)
        If s = "" Then
            Return defaultValue
        End If
        Dim o As Object
        Try
            o = System.Enum.Parse(t, s, True)
        Catch ex As System.ArgumentException
            '-- if the string representation provided doesn't match 
            '-- any known enum (case, we'll get this exception
        End Try
        If o Is Nothing Then
            Return defaultValue
        Else
            Return o
        End If
    End Function

    ''' <summary>
    ''' parses values of the form
    ''' name=true, name=false
    ''' </summary>
    Private Function ParseBoolean(ByVal initializeData As String, ByVal name As String, ByVal defaultValue As Boolean) As Boolean
        Dim m As Match = Regex.Match(initializeData, "(?<=" & name & "=)false|true", RegexOptions.IgnoreCase)
        If m.Success Then
            Return Boolean.Parse(m.Value)
        Else
            Return defaultValue
        End If
    End Function

    ''' <summary>
    ''' parses values of the form
    ''' name=3, name=28932
    ''' </summary>
    Private Function ParseLong(ByVal initializeData As String, ByVal name As String, ByVal defaultValue As Long) As Long
        Dim m As Match = Regex.Match(initializeData, "(?<=" & name & "=)\d+", RegexOptions.IgnoreCase)
        If m.Success Then
            Return Long.Parse(m.Value)
        Else
            Return defaultValue
        End If
    End Function

    ''' <summary>
    ''' parses values of the form 
    ''' name='data', name="data", name=data
    ''' </summary>
    Private Function ParseString(ByVal initializeData As String, ByVal name As String, ByVal defaultValue As String) As String
        Dim m As Match = Regex.Match(initializeData, "(?<=" & name & "=('|"")*)[^'"",]+", RegexOptions.IgnoreCase)
        If m.Success Then
            Return m.Value
        Else
            '-- check for the ='' ="" =, case (empty string)
            If Regex.IsMatch(initializeData, name & "=['"",]['""]*", RegexOptions.IgnoreCase) Then
                Return ""
            Else
                Return defaultValue
            End If
        End If
    End Function

#End Region

    Public Overloads Overrides Sub Write(ByVal o As Object)
        WriteMessage(FormatMessage(o.ToString, "", False))
    End Sub

    Public Overloads Overrides Sub Write(ByVal message As String)
        WriteMessage(FormatMessage(message, "", False))
    End Sub

    Public Overloads Overrides Sub Write(ByVal message As String, ByVal category As String)
        WriteMessage(FormatMessage(message, category, False))
    End Sub

    Public Overloads Overrides Sub Write(ByVal o As Object, ByVal category As String)
        WriteMessage(FormatMessage(o.ToString, category, False))
    End Sub

    Public Overloads Overrides Sub WriteLine(ByVal o As Object)
        WriteMessage(FormatMessage(o.ToString, "", True))
    End Sub

    Public Overloads Overrides Sub WriteLine(ByVal message As String)
        WriteMessage(FormatMessage(message, "", True))
    End Sub

    Public Overloads Overrides Sub WriteLine(ByVal message As String, ByVal category As String)
        WriteMessage(FormatMessage(message, category, True))
    End Sub

    Public Overloads Overrides Sub WriteLine(ByVal o As Object, ByVal category As String)
        WriteMessage(FormatMessage(o.ToString, category, 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

#Region "  Private Methods"

    Private Function FormatMessage(ByVal message As String, ByVal category As String, ByVal includeNewLine As Boolean) As String()
        Return New String() {GetIndent(), GetTimeStamp(), GetPidTid(), GetMethodName(), GetCategory(category), message, GetNewLine(includeNewLine)}
    End Function

    ''' <summary>
    ''' creates a new log filename in this format
    '''   "Directory \ FileNameTemplate"
    ''' </summary>
    Private Function CreateLogFileName(ByVal fileIndex As Long) As String
        Dim sb As New StringBuilder
        sb.Append(_FolderName)
        sb.Append(String.Format(_FileNameTemplate, fileIndex, DateTime.Now))
        Return sb.ToString
    End Function

    ''' <summary>
    ''' Check that no more than (n) log files will exist at any given time;
    ''' if more than (n) do exist, the oldest one is deleted
    ''' </summary>
    Private Sub EnforceFileThreshold()
        If _FileCountThreshold = 0 Then Return

        '-- get all the files in the current folder..
        Dim FileNames() As String
        If Path.GetExtension(_FileNameTemplate) = "" Then
            FileNames = Directory.GetFiles(_FolderName)
        Else
            '-- ..that end with whatever log extension was specified
            FileNames = Directory.GetFiles(_FolderName, "*" & Path.GetExtension(_FileNameTemplate))
        End If

        If FileNames.Length = 0 Then Return

        Dim FilesMatched As Integer = 0
        Dim OldestFileDate As DateTime = DateTime.MinValue
        Dim OldestFileName As String = ""

        '-- find all the files that match our specific log pattern 
        '-- (extension isn't specific enough
        Dim FilePattern As String = Regex.Replace(_FileNameTemplate, "{[^}]+?}", ".*?") & "$"
        Dim r As New Regex(FilePattern)
        Dim fi As FileInfo
        For Each FileName As String In FileNames
            If r.IsMatch(FileName) Then
                FilesMatched += 1
                fi = New FileInfo(FileName)
                If fi.CreationTimeUtc > OldestFileDate Then
                    OldestFileDate = fi.CreationTimeUtc
                    OldestFileName = FileName
                End If
            End If
        Next

        If FilesMatched > _FileCountThreshold Then
            File.Delete(OldestFileName)
        End If
    End Sub

    ''' <summary>
    ''' Opens the "current" log file; this can be either an
    ''' existing incomplete log file or a brand new log file
    ''' </summary>
    Private Sub OpenLogFile(ByVal messageLength As Long)
        '-- close any currently open log file, if any
        CloseLogFile()

        Dim FileName As String
        Dim LoopCount As Integer = 0

        Do While True

            LoopCount += 1

            '-- generate next log name in sequence (by date, index, etc)
            If _FileCountThreshold = 0 Then
                _FileIndex = 1
            Else
                _FileIndex += 1
                If _FileIndex > _FileCountThreshold Then
                    _FileIndex = 1
                End If
            End If
            FileName = CreateLogFileName(_FileIndex - 1)

            '-- see if next log file already exists
            If Not File.Exists(FileName) Then
                '-- this will be a new log file
                _FileLength = 0
                _FileCreationDate = DateTime.MinValue
                '-- if creating a new file, we need to make ABSOLUTELY
                '-- sure we haven't exceeded total allowed file count
                EnforceFileThreshold()
                Exit Do
            Else
                '-- existing log file; retrieve length and creation time
                Dim fi As New FileInfo(FileName)
                _FileLength = fi.Length
                _FileCreationDate = fi.CreationTimeUtc

                '-- has this log file exceeded valid length or age?
                If LogFileSizeMaxReached(messageLength) Or LogFileAgeMaxReached() Then
                    If _FirstLogFound Or (LoopCount > _FileCountThreshold) Then
                        File.Delete(FileName)
                        _FileLength = 0
                        Exit Do
                    End If
                Else
                    Exit Do
                End If
            End If
        Loop

        '-- this is an optimization for subsequent passes through the loop
        _FirstLogFound = True

        '-- at this point we're either.. 
        '-- A) opening a brand new logfile
        '-- B) appending to an existing logfile
        _sw = File.AppendText(FileName)
        _sw.AutoFlush = _AutoFlush
    End Sub

    Private Sub CloseLogFile()
        SyncLock Me
            If (Not _sw Is Nothing) Then
                _sw.Close()
                _sw = Nothing
            End If
        End SyncLock
    End Sub

    Private Function StringArrayLength(ByVal message As String()) As Long
        Dim ml As Long = 0
        For i As Integer = 0 To message.Length - 1
            ml += message(i).Length
        Next
        Return ml
    End Function

    Private Sub WriteMessage(ByVal message As String())
        Dim ml As Long = StringArrayLength(message)
        SyncLock Me
            If _sw Is Nothing Then
                OpenLogFile(ml)
            Else
                If LogFileSizeMaxReached(ml) Or LogFileAgeMaxReached() Then
                    OpenLogFile(ml)
                End If
            End If
            For i As Integer = 0 To message.Length - 1
                _sw.Write(message(i))
            Next
            _FileLength += ml
        End SyncLock
    End Sub

    Private Function GetMethodName() As String
        If _AddMethod Then
            Dim sf As New StackFrame(_StackFrameSkipCount)
            Dim mb As MethodBase = sf.GetMethod
            Dim sb As New StringBuilder
            sb.Append(mb.ReflectedType.FullName)
            sb.Append(".")
            sb.Append(mb.Name)
            sb.Append(_FieldSeparator)
            Return sb.ToString
        Else
            Return ""
        End If
    End Function

    Private Function GetIndent() As String
        Return New String(_IndentCharacter, (Me.IndentLevel * Me.IndentSize))
    End Function

    Private Function GetCategory(ByVal category As String) As String
        If category = "" Then
            Return ""
        Else
            Dim sb As New StringBuilder
            sb.Append(category)
            sb.Append(_FieldSeparator)
            Return sb.ToString
        End If
    End Function

    Private Function GetNewLine(ByVal includeNewLine As Boolean) As String
        If includeNewLine Then
            Return Environment.NewLine
        Else
            Return ""
        End If
    End Function

    Private Function GetPidTid() As String
        If _AddPidTid Then
            Dim sb As New StringBuilder
            sb.Append(Process.GetCurrentProcess.Id)
            sb.Append("/")
            sb.Append(AppDomain.GetCurrentThreadId)
            sb.Append(_FieldSeparator)
            Return sb.ToString
        Else
            Return ""
        End If
    End Function

    Private Function GetTimeStamp() As String
        If _TimeStampFormat = "" Then
            Return ""
        Else
            Dim sb As New StringBuilder
            sb.Append(DateTime.Now.ToString(_TimeStampFormat))
            sb.Append(_FieldSeparator)
            Return sb.ToString
        End If
    End Function

    Private Function LogFileAgeMaxReached() As Boolean
        If _FileAgeThreshold = 0 Then
            Return False
        Else
            If _FileCreationDate = DateTime.MinValue Then
                Return False
            End If
            Select Case _FileAgeUnit
                Case AgeUnit.Hours
                    Return _FileCreationDate < DateTime.UtcNow.AddHours(-_FileAgeThreshold)
                Case AgeUnit.Minutes
                    Return _FileCreationDate < DateTime.UtcNow.AddMinutes(-_FileAgeThreshold)
                Case AgeUnit.Months
                    Return _FileCreationDate < DateTime.UtcNow.AddMonths(-Convert.ToInt32(_FileAgeThreshold))
                Case AgeUnit.Weeks
                    Return _FileCreationDate < DateTime.UtcNow.AddDays(-(_FileAgeThreshold * 7))
                Case Else
                    '-- default to days
                    Return _FileCreationDate < DateTime.UtcNow.AddDays(-_FileAgeThreshold)
            End Select
        End If
    End Function

    Private Function LogFileSizeMaxReached(ByVal messageLength As Long) As Boolean
        If _FileSizeThreshold = 0 Then
            Return False
        Else
            Dim l As Long = messageLength + _FileLength
            Select Case _FileSizeUnit
                Case SizeUnit.Kilobytes
                    Return l > (_FileSizeThreshold * 1024)
                Case SizeUnit.Megabytes
                    Return l > (_FileSizeThreshold * 1048576)
                Case SizeUnit.Gigabytes
                    Return l > (_FileSizeThreshold * 1073741824)
                Case Else
                    '-- default to bytes
                    Return l >= _FileSizeThreshold
            End Select

        End If
    End Function

#End Region

    Protected Overrides Sub Finalize()
        Me.Close()
        MyBase.Finalize()
    End Sub

End Class

Posted by Jeff Atwood    View blog reactions

 

« On Software "Engineering" MS Language Equivalents »

 

Comments

Great bit of code.

I was thinking of doing the same sort of thing using the Logging block in the Enterprise Library. I found a 'rolling file sink' here:

http://blog.hishambaz.com/archive/2005/02/14/317.aspx

Are you a fan of the Enterprise Library stuff yourself?

Andy on March 7, 2005 07:53 AM

Well, if it has the word "Enterprise" in it, it's already kind of suspect ;)

I haven't worked with it much. I hear it's very heavy and extremely configuration intensive.

Jeff Atwood on March 7, 2005 04:16 PM

Ooh! Danke!

Dan F on March 8, 2005 06:18 PM

Hi. Thats some nice formatting for the code especially with the print and view plane. Are you using some formatting tool readily available or are you formatting this yourself?
Cheers.
Sushant

Sushant Bhatia on March 9, 2005 08:47 AM

If you click the "?" you'll find a link to the source of the code. It is very cool -- although the size of the code I posted is probably pushing the boundaries of what is sensible to do via JavaScript.

Jeff Atwood on March 9, 2005 10:36 AM

Very good!!! I had to put a synclock in the OpenLogFile. Could it be necessary in case of multi-threads applications?

SyncLock Me
_sw = File.AppendText(FileName)
_sw.AutoFlush = _AutoFlush
End SyncLock

Thanks

Stefano B on June 11, 2007 02:04 PM

First off thanks for sharing this code.

I tried using it in VS2005 and the close method kept erroring out on the closing of the stream. Not sure how to resolve this

kevin.s.gallagher@state.or.us

Kevin Gallagher on May 1, 2008 09:31 AM







(hear it spoken)


(no HTML)




Content (c) 2008 Jeff Atwood. Logo image used with permission of the author. (c) 1993 Steven C. McConnell. All Rights Reserved.