Saturday, July 07, 2007

Singleton Pattern - A Logger Example

The idea of a singleton is to have a class that has only one instance and a global point of access to the instance. Following code is a simple singleton C# implementation:
public class Singleton
{
private static Singleton instance = new Singleton();

private Singleton() { }

public static Singleton GetInstance()
{
return instance;
}
}
The above implementation creates a new static instance during the class initialization. The singleton instance is retrieved by a static GetInstance method. An alternative is expose the singleton object by read-only property without a setter. The code needs a little bit change if a lazy creation is required:
public class Singleton
{
private static volatile Singleton instance;
private static object syncObj = new Object();

private Singleton() {}

public static Singleton GetInstance()
{
if (instance == null)
{
lock (syncObj)
{
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
SyncLock in above code is for thread-safe consideration. In addition, the singleton class could be sealed if you don't like people to inherit from it and break the singleton principle.

Singleton pattern is very useful in some use cases. Here's a real life example. I needed a file logger for a simple web service application used in a small company. Enterprise library, logging application block, and even log4Net are just too heavy for such small application. In addition, the client is rejective to third-party tools.

I decided to write a file logger by myself. A stream writer was used, but I don't want multiple instances of stream writers being created and used at the same time. The IO is expensive and multiple accesses to a single file is not safe. Singleton came to play and only one instance of logger and one stream writer is created:
using System;
using System.IO;
using System.Configuration;
using System.Diagnostics;

class Program
{
static void Main(string[] args)
{
Logger.WriteLog("Jus a test.");
Logger.WriteLog(LogSeverity.Info, "Another test.");
Console.Read();
}
}

// A helper static class to get access the logger
public static class Logger
{
private static readonly FileLogger _logger = FileLogger.GetInstance();

public static void WriteLog(String log)
{
_logger.Write(log);
}
public static void WriteLog(LogSeverity logLevel, String log)
{
_logger.Write(logLevel, log);
}
}

// Enumeration for log severity
public enum LogSeverity { Error, Warning, Info, Debug };

// FileLogger class with Singleton pattern
internal sealed class FileLogger : IDisposable
{
private static FileLogger _instance = null; // Singleton object
private static object _syncLock = new object(); // Sync lock

internal StreamWriter _writer; // Stream writer writting to the log file
internal string _path = "C:\\Logs"; // Default folder
internal string _absolutePath; // C:\Logs\MachineName.log
internal string _fileName; // MachineName.log
internal int _logFileSize = 2; // New log file when its size bigger than 2 MB
internal LogSeverity _defaultSeverity = LogSeverity.Error; // Default severity

// Private constructor
private FileLogger()
{
GetConfigSettings();

_fileName = string.IsNullOrEmpty(Environment.MachineName) ?
"LogFile.log" : Environment.MachineName + ".log";
_absolutePath = _path + "\\" + _fileName;

if (!Directory.Exists(_path))
{
Directory.CreateDirectory(_path);
}

_writer = new StreamWriter(_absolutePath, true);
}

// Public static method to get Singleton object
public static FileLogger GetInstance()
{
if (_instance == null)
{
lock (_syncLock)
{
if (_instance == null)
{
_instance = new FileLogger();
}
}
}
return _instance;
}

// Check if the log file exceeds the maximum size
private void CheckFileSize()
{
if (_writer.BaseStream.Length >= (_logFileSize * 1024 * 1024))
{
this.Dispose();
string backupFile = string.Format("{0}\\{1}-{2}.log",
_path, Environment.MachineName, DateTime.Now.ToString("yyyyMMdd-HHmmsss"));
try
{
while (File.Exists(backupFile))
{
backupFile = backupFile.Replace(".log", "_1.log");
}
File.Move(_absolutePath, backupFile);
this.Dispose();
_writer = new StreamWriter(_absolutePath, true);
}
catch (Exception ex)
{
WriteToEventLog(ex.Message);
}
}
}

// Get default setting from configuraiton file
private void GetConfigSettings()
{
string appSetting = ConfigurationManager.AppSettings["FileLogger_Path"];
if (!string.IsNullOrEmpty(appSetting))
{
_path = appSetting;
}
int maxFileSize;
appSetting = ConfigurationManager.AppSettings["FileLogger_MaxFileSizeInMB"];
if (!string.IsNullOrEmpty(appSetting) && int.TryParse(appSetting, out maxFileSize))
{
_logFileSize = maxFileSize;
}
appSetting = ConfigurationManager.AppSettings["FileLogger_DefaultSeverity"];
if (!string.IsNullOrEmpty(appSetting))
{
try
{
_defaultSeverity = (LogSeverity)Enum.Parse(typeof(LogSeverity), appSetting);
}
catch (Exception ex)
{
WriteToEventLog(ex.Message);
}
}
}

// Clean up the stream writer
public void Dispose()
{
lock (_syncLock)
{
if (_writer != null)
{
_writer.Dispose();
}
}
}

// Write with default log severity
public void Write(string msg)
{
Write(_defaultSeverity, msg);
}

// Write message to log file
public void Write(LogSeverity logSeverity, string msg)
{
lock (_syncLock)
{
try
{
if (_writer.BaseStream == null)
{
_writer = new StreamWriter(_absolutePath, true);
}
CheckFileSize();
_writer.WriteLine("[{0}] at {1} -- {2}",
logSeverity.ToString(), DateTime.Now.ToString(), msg);
_writer.Flush();
}
catch (Exception ex)
{
WriteToEventLog(ex.Message);
}
}
}

private void WriteToEventLog(string message)
{
string source = "Logger";
string log = "Application";

try
{
if (!EventLog.SourceExists(source))
EventLog.CreateEventSource(source, log);

EventLog.WriteEntry(source, message, EventLogEntryType.Error);
}
catch { }
}
}