본문 바로가기

언어/C#

LogString: A Simple C# 2 Application Event Logging Class

출처: https://www.codeproject.com/Articles/15364/LogString-A-Simple-C-Application-Event-Logging-C


Introduction

When interfacing to real-world devices, it is typical to have one or more background processing tasks responsible for collecting data and/or controlling the devices. Monitoring these tasks can be done in a variety of ways, some of which can involve complex GUI components for status reporting and user interaction for control. As the name implies, the LogString class provides only a subset of this type of functionality.

There are many logging frameworks and APIs available, along with tools to deal with the Windows Event Log. I counted no less than 30 related CodeProject articles. While you could possibly use some of these tools, their functionality is not generally applicable to the requirements described below. That said, I should note that the project LogString was developed for uses log4net [^] for its system logging. The difference, of course, is that the two logging facilities (log4net and LogString) are used for completely different purposes.

This article also details how I used some basic .NET 2.0 Framework capabilities in the solution. There are no tricks or undocumented system features here. You can think of this as a C# 2 beginner's tutorial that shows some of the language features that would be useful in your day-to-day development work.

Requirements

The development of the LogString class came about because the application needed the ability for the user to monitor the activity of multiple background processing tasks in real-time. In other words, they needed to be able to view processing events as they happened. This is shown here:

The specific requirements are:

  • Multiple application-based log strings ("Task-1", "Task-2", etc.).
  • Each application log is relatively low volume. For my purposes, this meant that there are typically no more than 1 or 2 log events every couple of minutes.
  • The total size of a logging string is limited to the last 750-1000 event entries. Older events are not needed and are permanently discarded. 
    [Note: This is the current LogString behavior, but a more practical implementation will have the ability to maintain a much longer log history.]
  • Multiple background processing tasks and viewers shall be able to simultaneously access each log, i.e. thread-safe operation.
  • Log viewers shall have the ability receive asynchronous notification that the log has been updated.
  • Simple persistence model.
  • The primary viewer is a read-only multi-line TextBox control with the latest log entries shown at the top.

LogString Class

LogString is implemented as a single class and has a very simple interface:

Static Methods


static LogString GetLogString(string name);   // Get a LogString instance

static void PersistAll();               // Save all LogString instances

static void ClearAll();                 // Clear the contents of all LogString instances

static void RemoveLogString(string name);     // Remove the named LogString instance

Instance Methods

void Add(string message);                    // Add a message

void Persist();                              // Save this LogString instance

void Clear();                                // Clear this LogString instance

Instance Properties

string Log;                                  // This is the log string 

delegate void LogUpdateDelegate();           // The notification delegate
event LogUpdateDelegate OnLogUpdate;         // The update event

Option properties: See table.

Type</strong />Property NameDescriptionDefault Value
boolReverseOrderIf true, add new entries to the start of the log. This makes viewing real-time updates easier because new items appear at the top while older entries scroll off the bottom. If set to false, new entries are appended to the end of the log text.true
boolLineTerminateTerminate each new entry with CRLF.true
boolTimeStampAdd timestamp to each log entry. If false, no timestamp is added.true
intMaxCharsMaximum number of characters allowed in the log. Once the log string reaches this size, the oldest text is removed: From the end if ReverseOrder is true, from the start if ReverseOrder is false.32000

The GetLogString() method is used to access a named LogString instance:

LogString myLogger = LogString.GetLogString("Task1");

The returned object is a singleton. A background processing task would add a log entry by calling the Add()method:

myLogger.Add("Something important happened!");

A monitoring task accesses the entire contents of the log though the Log property. If textBox1 is a TextBoxcomponent, the log contents would be viewed with:

textBox1.Text = myLogger.Log

Automatic updates to a viewer are achieved by adding a LogUpdateDelegate delegate to the OnLogUpdateevent of a LogString instance:

myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);

The LogUpdate function updates the TextBox component through the Invoke() method. These details are discussed below.

Each log string can be individually persisted with the Persist() instance method, but it would be more typical to use the PersistAll() static method when the application exits (or e.g. at timed intervals) to persist all logs with a single call:

LogString.PersistAll();

An individual log can be cleared with the Clear() method or all logs with ClearAll().

The LogStringTestApp project, which includes the LogString class, demonstrates most of this functionality, including logging from background threads.


static LogString GetLogString(string name);   // Get a LogString instance

static void PersistAll();               // Save all LogString instances

static void ClearAll();                 // Clear the contents of all LogString instances

static void RemoveLogString(string name);     // Remove the named LogString instance

Instance Methods

void Add(string message);                    // Add a message

void Persist();                              // Save this LogString instance

void Clear();                                // Clear this LogString instance

Instance Properties

string Log;                                  // This is the log string 

delegate void LogUpdateDelegate();           // The notification delegate
event LogUpdateDelegate OnLogUpdate;         // The update event

Option properties: See table.

Type</strong />Property NameDescriptionDefault Value
boolReverseOrderIf true, add new entries to the start of the log. This makes viewing real-time updates easier because new items appear at the top while older entries scroll off the bottom. If set to false, new entries are appended to the end of the log text.true
boolLineTerminateTerminate each new entry with CRLF.true
boolTimeStampAdd timestamp to each log entry. If false, no timestamp is added.true
intMaxCharsMaximum number of characters allowed in the log. Once the log string reaches this size, the oldest text is removed: From the end if ReverseOrder is true, from the start if ReverseOrder is false.32000

The GetLogString() method is used to access a named LogString instance:

LogString myLogger = LogString.GetLogString("Task1");

The returned object is a singleton. A background processing task would add a log entry by calling the Add()method:

myLogger.Add("Something important happened!");

A monitoring task accesses the entire contents of the log though the Log property. If textBox1 is a TextBoxcomponent, the log contents would be viewed with:

textBox1.Text = myLogger.Log

Automatic updates to a viewer are achieved by adding a LogUpdateDelegate delegate to the OnLogUpdateevent of a LogString instance:

myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);

The LogUpdate function updates the TextBox component through the Invoke() method. These details are discussed below.

Each log string can be individually persisted with the Persist() instance method, but it would be more typical to use the PersistAll() static method when the application exits (or e.g. at timed intervals) to persist all logs with a single call:

LogString.PersistAll();

An individual log can be cleared with the Clear() method or all logs with ClearAll().

The LogStringTestApp project, which includes the LogString class, demonstrates most of this functionality, including logging from background threads.


static LogString GetLogString(string name);   // Get a LogString instance

static void PersistAll();               // Save all LogString instances

static void ClearAll();                 // Clear the contents of all LogString instances

static void RemoveLogString(string name);     // Remove the named LogString instance

Instance Methods

void Add(string message);                    // Add a message

void Persist();                              // Save this LogString instance

void Clear();                                // Clear this LogString instance

Instance Properties

string Log;                                  // This is the log string 

delegate void LogUpdateDelegate();           // The notification delegate
event LogUpdateDelegate OnLogUpdate;         // The update event

Option properties: See table.

Type</strong />Property NameDescriptionDefault Value
boolReverseOrderIf true, add new entries to the start of the log. This makes viewing real-time updates easier because new items appear at the top while older entries scroll off the bottom. If set to false, new entries are appended to the end of the log text.true
boolLineTerminateTerminate each new entry with CRLF.true
boolTimeStampAdd timestamp to each log entry. If false, no timestamp is added.true
intMaxCharsMaximum number of characters allowed in the log. Once the log string reaches this size, the oldest text is removed: From the end if ReverseOrder is true, from the start if ReverseOrder is false.32000

The GetLogString() method is used to access a named LogString instance:

LogString myLogger = LogString.GetLogString("Task1");

The returned object is a singleton. A background processing task would add a log entry by calling the Add()method:

myLogger.Add("Something important happened!");

A monitoring task accesses the entire contents of the log though the Log property. If textBox1 is a TextBoxcomponent, the log contents would be viewed with:

textBox1.Text = myLogger.Log

Automatic updates to a viewer are achieved by adding a LogUpdateDelegate delegate to the OnLogUpdateevent of a LogString instance:

myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);

The LogUpdate function updates the TextBox component through the Invoke() method. These details are discussed below.

Each log string can be individually persisted with the Persist() instance method, but it would be more typical to use the PersistAll() static method when the application exits (or e.g. at timed intervals) to persist all logs with a single call:

LogString.PersistAll();

An individual log can be cleared with the Clear() method or all logs with ClearAll().

The LogStringTestApp project, which includes the LogString class, demonstrates most of this functionality, including logging from background threads.



Multiple Singletons

static Hashtable is used to maintain all named log strings:

private static Hashtable m_LogsTable = new Hashtable();

public static LogString GetLogString(string name)
{
    // If it exists, return the existing log.
    if (m_LogsTable.ContainsKey(name)) return (LogString)m_LogsTable[name];
    // Create and return a new log.
    LogString rv = new LogString(name);
    m_LogsTable.Add(name, rv); // add to table
    return rv;
}

// Constructor
private LogString(string name)
{
    m_strName = name;
    ReadLog();  // Read existing
}

Each LogString instance returned is a singletonLogString instances can be removed from the table with the RemoveLogString method. This would only need to be done if you were creating many uniquely named logs so that the table would not fill up with unused instances.

Notice that the LogString constructor reads its log file (if it exists). This will automatically restore the contents of the named log string when it is instantiated.

Access Locking

In order to provide thread safe operation, whenever the internal log string is read or modified, it must be locked.

public void Clear()
{
    lock (m_strLog) // lock resource
    {
        m_strLog = string.Empty;
    }
    WriteLog(); // This will remove the log file
    // Notify listeners of the update
    if (OnLogUpdate != null) OnLogUpdate();
}

When one thread is inside the lock code block, another thread that executes a lock on the same object will be blocked until the other thread exits their block. I have used the C# lock syntax here instead of the more general purpose Mutex (or Monitor). There are several reasons for this:

  1. I do not need cross-application resource locking, which is what a Mutex/Monitor class can provide.
  2. The lock code is cleaner. You do not have to surround the resource use code with WaitOne()/ReleaseMutex() calls.
  3. You don't have to worry about exceptions being thrown in the locked code block. The lock statement uses try..catch..finally to ensure that the lock is properly released. As such, you can also call return from inside a lock code block.
  4. For more details about access locking, see Thread Synchronization (C# Programming Guide) [^].

Event Generation

public delegate /event pair are made available to clients that want to be notified that a LogStringinstance has been updated (i.e. the Log string has been modified).

public delegate void LogUpdateDelegate();
public event LogUpdateDelegate OnLogUpdate;

When a change to the log string has been made, all delegates that have been added to the event will be notified when LogString calls OnLogUpdate when there are delegates present:

if (OnLogUpdate != null) OnLogUpdate();

A client would use the following to update a TextBox component:

private System.Windows.Forms.TextBox txtLog;
...
myLogger.OnLogUpdate += new LogString.LogUpdateDelegate(this.LogUpdate);
...

private delegate void UpdateDelegate();
private void LogUpdate()
{
    Invoke(new UpdateDelegate(
        delegate
        {
            txtLog.Text = myLogger.Log;
        })
    );
}

The Invoke() call is necessary so that the UI component updates can occur from other threads. The Anonymous delegate shown here is a new .NET 2.0 feature (see C#2 Anonymous Methods [^]) . Not having to define an extra delegate method makes the code cleaner and easier to follow.

Conclusion

If you have requirements that are close to what I've outlined, then you can use the LogString class as is or modify it for your own purposes. Many enhancements are possible. The C# language tools described are fairly basic and should be in your everyday tool box. Enjoy!

History

  • 28th August, 2006: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


LogStringTestApp_src.zip