Logger

Of course I do realize that most of the time software engineers will use a gold standard logger such as Log4Net. I have included this because the Logger object appears in my Server class. I did not feel that it would be acceptable to write logging code using something like Log4Net and then tell the viewer to go through setting it all up if they wanted logging.

This really just started as a challenge...a fellow engineer that I worked with warned me to never try and create my own logger and he was right about that. The problem is that, if you have created an inefficient logger class and you are debugging time sensitive code, you can see poor behavior or timing issues and not realize that your home-brew logger is what is causing the trouble.

So, the goal was to see if I could create a class that does not kill system resources due to unoptimized I/O. Bless the Dot Net engineers at Microsoft for giving us the 'using' statement (so that we don't have to wory about disposing unmanaged resources) and we have the StreamWriter class to do the real work.

As it turns out, StreamWriter is very good at what it does. In one project where I used this logger class, the main code was processing at times, hundreds of card swipes at terminals while also running queries against a very busy database. I had around 8 instances of this class running simultaneously.

I expected to see substantial CPU load due to all of the I/O. Well, there wasn't. With all of those loggers running and writing thousands of lines to text files, the CPU load never went over 1 or 2 percent.

I think that another aspect that made my design work well is the way that I designed the buffer cache. I did not think that it would make much of a difference but it did. On my dev machine the logger would write chunks of around 100 lines or so at a time, while log entries where coming in fast. This is what I was going for to reduce I/O cost.

What that cache code does is copy the buffer to another collection so that the buffer is free and can accept more incoming messages.

There are a few ways to clone a collection such as using 'ICloneable' but I just went with the super easy way of just assigning the cache as 'new' and passing the queue as the parameter. Doing it that way copies the present state of the queue at that moment in time.

The cool thing about this is that it is just one, simple class and it is under 200 lines. It is very convenient to drop it into a project quickly. I know that the code is not impressive by any means, but it works well.


using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace LoggerTest
{
    class Logger
    {
        private int logIndex;
        private int limit;
        private bool running;
        private bool writing;
        private bool _auto;
        private bool _rotate;
        private bool _date;
        private string _path;
        private object lockQueue;
        private object lockWrt;
        private string dtDisplay;
        private string format;
        private string fileName;
        private string directory;
        private List<string> cache;
        private List<string> queue;
        private Task logThread;
        private FileInfo fileDetails;
        private DateTime dt;

        public Logger(string path, bool auto, bool rotate, bool date)
        {
            _auto = auto;
            _rotate = rotate;
            _date = date;
            _path = path;
            logIndex = 0;
            limit = 10024000;
            running = true;
            writing = false;
            lockQueue = new object();
            lockWrt = new object();
            queue = new List<string>();
            directory = Path.GetDirectoryName(_path);
            fileName = Path.GetFileNameWithoutExtension(_path);
            createLogFile();
            format = "ddd MMM d yyyy HH:mm:ss";
            logThread = Task.Factory.StartNew(new Action(processQueue), TaskCreationOptions.LongRunning);
        }

        private void createLogFile()
        {
            if (!Directory.Exists(directory))
            {
                Directory.CreateDirectory(directory);
            }

            if (_rotate) // see what is already there by counting the files
            {
                foreach (string file in Directory.GetFiles(directory))
                {
                    if (file.Contains(fileName)) logIndex++;
                }
            }

            if (_auto)
            {
                File.Create(_path).Dispose();
            }

            else if (!_auto && !File.Exists(_path))
            {
                File.Create(_path).Dispose();
            }
        }

        public void createNewLogFile() // this can be called while in operation
        {
            while (cache.Count != 0 || queue.Count != 0)
            {
                Thread.Sleep(100);
            }

            File.Delete(_path);
            File.Create(_path).Dispose();
        }

        public void log(string message)
        {
            lock (lockQueue)
            {
                if (_date)
                {
                    if (message == "")
                        queue.Add(message); // don't date blank lines
                    else
                    {
                        dt = DateTime.Now;
                        dtDisplay = dt.ToString(format) + "." + dt.Millisecond.ToString().PadLeft(3, '0');

                        queue.Add(dtDisplay + " " + message);
                    }
                }
                else
                    queue.Add(message);
            }
        }

        private void processQueue()
        {
            int msgCount = 0;

            while (running)
            {
                msgCount = queue.Count;
                
                if (msgCount != 0)
                {
                    cache = new List<string>(queue);    // make a copy

                    while (writing)                     // only write new lines if not presently writing
                    {
                        Thread.Sleep(100);     
                    }

                    writeLog();

                    queue.RemoveRange(0, (cache.Count)); // clear written messages from the msgQueue
                    cache.Clear();
                }

                Thread.Sleep(150);
            }
        }

        private void writeLog()
        {
            long fileSize = 0;

            if (_rotate)
            {
                fileDetails = new FileInfo(_path); // log rotation
                fileSize = fileDetails.Length;

                if (fileSize > limit)
                {
                    logIndex++;
                    if (logIndex > 10) logIndex = 1;

                    File.Copy(_path, directory + @"\" + fileName + logIndex + ".txt", true);
                    File.Delete(_path);
                    File.Create(_path).Dispose();
                }
            }

            writing = true;
            using (StreamWriter file = File.AppendText(_path))
            {
                lock (lockWrt)
                {
                    foreach (string message in cache)
                    {
                        file.WriteLine(message);
                    }
                }
            }
            writing = false;
        }
    }
}