How to Write a Simple Logging System

This project came to me when I needed a way to view messages throughout the execution of my code and I was unable to rely on printing to the console while running a Windowed Application. I had used the debugger, until began to run into loops with hundreds of iterations. It had been suggested to me to use a logging tool.

I have grown accustomed to having a logging tool build into my IDE from using Android Studio’s LogCat. For my C# projects I use Visual Studio Code. While there is no logger integrated by default, there are dozens of extensions that can be downloaded an installed built by other developers.

I decided to develop my own as well. I approached the program in the most minimalist of fashions. The only thing I would consider a feature is the ability to the type and destination of the stream. The class is less than 100 lines of code, and that’s partially due to the excessive formatting of the Log string.

SimpleLogger

  1. /**
  2. The MIT License (MIT)
  3. Copyright © 2019 <John L. Mooney>
  4.  
  5. Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  6. and associated documentation files (the “Software”), to deal in the Software without restriction,
  7. including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
  9. subject to the following conditions:
  10.  
  11. The above copyright notice and this permission notice shall be included in all copies or substantial 
  12. portions of the Software.
  13.  
  14. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  15. TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  16. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  17. CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  18. DEALINGS IN THE SOFTWARE.
  19. */
  20. //
  21. // SimpleLogger.cs
  22. //
  23. using System;
  24. using System.IO;
  25. using System.Text;
  26.  
  27. namespace SimpleLogger
  28. {
  29.     public enum LogLevel
  30.     {
  31.         info,
  32.         debug,
  33.         warning,
  34.         error
  35.     }
  36.  
  37.     public class SimpleLogger
  38.     {
  39.         private static SimpleLogger logger = null;
  40.  
  41.         private static bool initialized = false;
  42.         private Stream logStream;
  43.  
  44.         private SimpleLogger(Stream stream)
  45.         {
  46.             this.logStream = stream;
  47.         }
  48.  
  49.         public static SimpleLogger GetLogger() 
  50.         {
  51.             if(!initialized)
  52.             {
  53.                 throw new UninitializedException("Initialize the Logger first.");
  54.             }
  55.             return logger;
  56.         }
  57.  
  58.         public static void Initialize(Stream stream)
  59.         {
  60.             if(!initialized)
  61.             {
  62.                 logger = new SimpleLogger(stream);
  63.                 initialized = true;
  64.             }
  65.         }
  66.  
  67.         public void Log(LogLevel level, string tag, string msg)
  68.         {
  69.             string date = DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss");
  70.             string strA = string.Format("[{0}]", date);
  71.             string strB = string.Format("<{0}>", level.ToString());
  72.             string strC = string.Format("::{0}::", tag);
  73.             string strD = string.Format("\"{0}\"", msg);
  74.             string str0 = string.Format("{0, -20}", strA);
  75.             string str1 = string.Format("{0, -10}", strB);
  76.             string str2 = string.Format("{0, -16}", strC);
  77.             string str3 = string.Format("{0, -20}", strD);
  78.             string str4 = Environment.NewLine;
  79.  
  80.             string fullmsg = string.Format("{0} {1} {2} {3}{4}",
  81.                 str0, str1, str2, str3, str4);
  82.             ToStream(fullmsg);
  83.         }
  84.  
  85.         private void ToStream(string msg)
  86.         {
  87.             byte[] text = new UTF8Encoding(true).GetBytes(msg);
  88.             logStream.Write(text, 0, text.Length);
  89.             logStream.Flush();
  90.         }
  91.     }
  92. }

Category Levels of Logging

I created four unique labels to categorize different levels of log statements. I chose the the terms based on what I have used in alternative logging systems. I enumerated them in an enum structure entitled “LogLevel.”
  1. public enum LogLevel
  2. {
  3.     info,
  4.     debug,
  5.     warning,
  6.     error
  7. }

Singleton

I followed the “Singleton” pattern by ensuring that there is only one static instance of the SimpleLogger to be shared among all who want to use the logger. The static instance must be initialized to null for this pattern to work. The rest is done through the implement of the Initialize() and GetLogger() methods.

The Initialize() method accepts a Stream derived object and passes it to the private constructor to set the member variable, logStream, thereby instantiating an static object of SimpleLogger. It also sets the boolean flag, “initialized,” to true so that It cannot be initialized more than once.

GetLogger() is the mechanism for obtaining the SimpleLogger instance, since there are no public constructors. when the method is called there is a check to see if the SimpleLogger has been initialized. If it has, then an object exists then it is returned to the calling function. In the alternative case, it throws an exception informing the user that the Logger needs to be initialized

UninitializedException.cs

I implemented a custom Exception class to represent the exception that should be thrown when a developer attempts to retrieve an uninitialized SimpleLogger instance. This is a skeleton exception class that provides no custom service, but allows for a specifically appropriate name and expansion in the future.

  1. /**
  2. The MIT License (MIT)
  3. Copyright © 2019 <John L. Mooney>
  4.  
  5. Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  6. and associated documentation files (the “Software”), to deal in the Software without restriction,
  7. including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
  9. subject to the following conditions:
  10.  
  11. The above copyright notice and this permission notice shall be included in all copies or substantial 
  12. portions of the Software.
  13.  
  14. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
  15. TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  16. THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
  17. CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  18. DEALINGS IN THE SOFTWARE.
  19. */
  20. //
  21. // UninitializedException.cs
  22. //
  23. using System;
  24.  
  25. namespace SimpleLogger
  26. {
  27.     public class UninitializedException : Exception
  28.     {
  29.         public UninitializedException()
  30.         {
  31.         }
  32.  
  33.         public UninitializedException(string message)
  34.             : base(message)
  35.         {
  36.         }
  37.  
  38.         public UninitializedException(string message, Exception inner)
  39.             : base(message, inner)
  40.         {
  41.         }
  42.     }
  43. }

Tag

Before I discuss the Log() method and it’s format, I want to explain one of elements of the message, the tag.

The Log’s tag is meant to give more context to the message than the log level alone. Supplying the log statement with a custom tag allows for easier message filtering. A personal example of this is when I code on a project with others, the others and myself will often pass in our respective first name into the tag parameter. This provides us with the ability to identify the logged message by owner through use of the tag. There are better and more practical uses. Another is example could be logging in multi-threaded application. If each thread has a identifier (string or numeric ID), then the messages could be tagged with the thread’s identifier, allowing you to trace the behavior and events of each thread.

Format and Log

SimpleLogger’s Log() method is what allows the user to output the log messages to the specified stream. As stated earlier in the post, I was a bit excessive in the formatting of the log message. I designed the message to provide the date and time of the message, then the log level, followed by a “tag” and finally the message intended to be logged.

  1. public void Log(LogLevel level, string tag, string msg)
  2. {
  3.     string date = DateTime.Now.ToString("MM/dd/yyyy hh:mm:ss");
  4.     string strA = string.Format("[{0}]", date);
  5.     string strB = string.Format("<{0}>", level.ToString());
  6.     string strC = string.Format("::{0}::", tag);
  7.     string strD = string.Format("\"{0}\"", msg);
  8.     string str0 = string.Format("{0, -20}", strA);
  9.     string str1 = string.Format("{0, -10}", strB);
  10.     string str2 = string.Format("{0, -16}", strC);
  11.     string str3 = string.Format("{0, -20}", strD);
  12.     string str4 = Environment.NewLine;
  13.  
  14.     string fullmsg = string.Format("{0} {1} {2} {3}{4}",
  15.         str0, str1, str2, str3, str4);
  16.     ToStream(fullmsg);
  17. }

To explain my verbosity. I wanted to create a string that would allow me to be able to easily identify which element of the log message is which. My purpose was to able to perform searches and filters using unique formatting surrounding each element (e.g. “[]” around the date/time or “::” on both sides of the tag). These distinguishing factors assist in parsing.

The component string variables all start with the substring “str” and are followed by an alpha or a numeric. The alpha are used to tack on the distinguishing characteristics of the log message element. The numeric are used to format the alpha strings within right-aligned fixed width space providing the log file with the illusion of columns. “str4” is used to shorten the constant for the platform independent new line. “fullmsg” is a concatenation of all the other strings, which are then passed to the ToStream() method to be written to the stream.

ToStream( )

Simple method
  1. Take the string argument and convert it to bytes
  2. Write the bytes out to the stream
  3. Flush the steam to prevent the bytes from hanging around in memory
  1. private void ToStream(string msg)
  2. {
  3.     byte[] text = new UTF8Encoding(true).GetBytes(msg);
  4.     logStream.Write(text, 0, text.Length);
  5.     logStream.Flush();
  6. }

Example Usages

Simple Example

/**
The MIT License (MIT)
Copyright © 2019 <John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial 
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
//
// Program.cs
//
using System;
using System.IO;

namespace SimpleLogger
{
    class Program
    {
        static void Main(string[] args)
        {
            FileStream stream = new FileStream(@"logs\simple.log", FileMode.Append,
                FileAccess.Write, FileShare.ReadWrite, 4096); 
            SimpleLogger.Initialize(stream);
            SimpleLogger logger  = SimpleLogger.GetLogger();
            
            logger.Log(LogLevel.warning, "Soup Nazi", "No soup for you");
            logger.Log(LogLevel.info, "TRON", "Greetings Program");
            logger.Log(LogLevel.warning, "Arithmatic", "Possible divide by zero");
            logger.Log(LogLevel.error, "Coverage", "Some paths may never be executed");
            logger.Log(LogLevel.info, "TRON", "I fight for the users");
            logger.Log(LogLevel.debug, "Edge case", "This line of code should never be reached");
        }
    }
}

The output should look something like this:

[11/07/2019 07:15:07] <warning> ::Soup Nazi:: "No soup for you"
[11/07/2019 07:15:07] <info> ::TRON:: "Greetings Program"
[11/07/2019 07:15:07] <warning> ::Arithmatic:: "Possible divide by zero"
[11/07/2019 07:15:07] <error> ::Coverage:: "Some paths may never be executed"
[11/07/2019 07:15:07] <info> ::TRON:: "I fight for the users"
[11/07/2019 07:15:07] <debug> ::Edge case:: "This line of code should never be reached"

A More Thorough Example

This is a GUI application so the main mostly consists of of boiler-plate. The important thing to take from this file is that a FileStream is created here followed by the initialization of the SimplerLogger. Here the the Logger will be initialized once and before anyone attempts to use it.

Program.cs

/**
The MIT License (MIT)
Copyright © 2019 <John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
//
// Program.cs
//
using System;
using System.IO;
using System.Windows.Forms;

namespace SimpleLogger
{
static class Program
{
[STAThread]
static void Main()
{
FileStream logStream = new FileStream(@"logs/squares.log",
FileMode.Append, FileAccess.Write, FileShare.ReadWrite, 4096);
SimpleLogger.Initialize(logStream);

Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Squares(128));
}
}
}

The Square class is an extension of the PictureBox class. I used it to make square boxes and painted them solid colors and named them by their respective color. The name, color and side length are passed in by construct by the Squares object that generates the boxes for its window.

Square has static instance of SimpleLogger called “logger.” To prevent every instance of Square from calling GetLogger() upon construction, I put the assignment logger = GetLogger() in a static constructor.

The reason this is a better example of the uses of SimpleLogger is because there are more strategic placements for logging. I created events for MouseClick, MouseEnter, MouseLeave to demonstrate logging based on an event. As mentioned earlier in the post, I opted to tag the logs by the name of the object. When I click a red colored square in the form window, I can verify that the event triggered is coming from the correct object using the log file (or the console).

/**
The MIT License (MIT)
Copyright © 2019 <John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial 
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Drawing;
using System.Windows.Forms;

namespace SimpleLogger
{
    public class Square : PictureBox
    {
        private static SimpleLogger logger;
        private new string Name{get; set;}
        private Brush brush;

        static Square()
        {
            try
            {
                logger = SimpleLogger.GetLogger();
            }
            catch(Exception e)
            {
                MessageBox.Show(e.Message);
                Environment.Exit(1);
            }
        }

        public Square(string name, Color color, int side)
        {
            Name = name;
            brush = new SolidBrush(color);
            Size = new Size(side, side);
            logger.Log(LogLevel.warning, "Square", "Initializing Square to name of " + Name);
            Init();
        }

        private void Init()
        {
            Paint += new PaintEventHandler(OnPaint);
            MouseClick += new MouseEventHandler(OnClickedMe);
            MouseEnter += new EventHandler(OnEnter);
            MouseLeave += new EventHandler(OnLeave);
        }

        private void OnPaint(object sender, PaintEventArgs e)
        {
            Point origin = new Point(0, 0);
            Rectangle rect = new Rectangle(origin, Size);
            e.Graphics.FillRectangle(brush, rect);
            logger.Log(LogLevel.debug, "Programmer", "Rectange: " + rect);
        }

        public override string ToString()
        {
            return Name;
        }

        public void OnClickedMe(object sender, MouseEventArgs e)
        {
            logger.Log(LogLevel.debug, sender.ToString(), "Hey, that tickles.");
        }

        public void OnEnter(object sender, EventArgs e)
        {
            logger.Log(LogLevel.info, sender.ToString(), "I see you");
        }

        public void OnLeave(object sender, EventArgs e)
        {
            logger.Log(LogLevel.info, sender.ToString(), "Where did you go?");
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }
    }
}

The Squares class is a form that displays a grid of Square objects. It logs the creation of each Square object tagging it “Squares.”

/**
The MIT License (MIT)
Copyright © 2019 &lt;John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial 
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace SimpleLogger
{
    public class Squares : Form
    {
        private int SquareSide;
        private List&lt;Square> squares;
        private List&lt;Point> Coords;
        private static SimpleLogger logger;
        private static Color[] colors = {Color.Red, Color.Aqua, Color.Yellow, Color.Green,
                        Color.Blue, Color.Brown, Color.Crimson, Color.Cyan};
        private static string[] names = {"Red", "Aqua", "Yellow", "Green", "Blue", "Brown",
                            "Crimson", "Cyan"};

        static Squares()
        {
            try
            {
                logger = SimpleLogger.GetLogger();
            }
            catch(Exception e)
            {
                MessageBox.Show(e.Message);
                Environment.Exit(1);
            }
        }

        public Squares(int side)
        {
            SquareSide = side;
            squares = new List&lt;Square>();
            Coords = new List&lt;Point>(); 
            Init();
        } 

        private void Init()
        {
            int index = 0;
            this.ClientSize = new Size(512, 512);
            this.Text = "LogTesting";
            int col = 4;
            int row = 4;
            Point start = new Point(0, 0);
            for(int i = 0; i &lt; col; i++)
            {
                for(int j = 0; j &lt; row; j++)
                {
                    Square sqr = new Square(names[index], colors[index], SquareSide);
                    Controls.Add(sqr);
                    squares.Add(sqr);
                    logger.Log(LogLevel.info, "Squares", "Just added a square to the Form. This Square's name is " + names[index]);
                    index = (index + 1) % colors.Length;
                    Point coord = new Point((i * SquareSide), (j * SquareSide));
                    Coords.Add(coord);
                }

            }
            InitPlacement();
        }

        public void InitPlacement()
        {
            int index = 0;
            foreach(Square square in squares)
            {
                square.Location =  Coords[index++];
            }
        }
        
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.