TechHui

Hawaiʻi's Technology Community

The topics I have been selecting to write about are a reflection of things that have come up over the previous week.  The topic this week is logging.  Logging is an extremely valuable tool when developing and debugging software.  It can make the difference between a thirty second scan of a text file and several hours of debugging.  Given the importance of logging there have been at least a few attempts at creating logging applications.  Log4xxx type loggers have tons of neat features.  As impressive is the feature list is, learning how to program a logger probably is not the most fun a programmer can have.  In the end, loggers should log information and then get out of the way.  Loggers should be easy to understand and easy to extend.  Loggers should not contain code to log to every possible log sink ever requested so that the feature list look more impressive.  It should be possible to write code for a specific type of sink and drop that code into projects when it is needed and cut it out when it is not.  This post covers the design for a very simple logging system, one that has proven itself over the years.

 

The logger interface

What the logger can do is described in this interface.  What may immediately stand out is that there is a specific method for each type of logging to be done.  Why is that?  Primarily it is due to making sure that any implementation of this interface provides exactly the same functionality, making the implementations interchangeable with each other.  Less obvious is the introduction of the TODO method.  Often code will incur some level of technical debt, the addition of a TODO log allows developers to track it and also to see how often code with technical debt is executed which gives an idea of the impact changing that code may have. 

 

    /// <summary>

    /// Declares the methods available for logging

    /// </summary>

    public interface ILogger

    {

        /// <summary>

        /// Debug messages should be used for a noisy spew of data that would help figure out specific problems

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        void Debug(string message);

 

        /// <summary>

        /// Debug messages should be used for a noisy spew of data that would help figure out specific problems

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        void Debug(string format, params object[] args);

 

        /// <summary>

        /// Trace messages should be used to indicate when you have entered or left a section of code

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        void Trace(string message);

 

        /// <summary>

        /// Trace messages should be used to indicate when you have entered or left a section of code

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        void Trace(string format, params object[] args);

 

        /// <summary>

        /// Info messages provide minor details - the results of specific actions, etc.

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        void Info(string message);

 

        /// <summary>

        /// Info messages provide minor details - the results of specific actions, etc.

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        void Info(string format, params object[] args);

 

        /// <summary>

        /// Should only be used to indicate that the app is about to crash hard.

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        void Fatal(string message);

 

        /// <summary>

        /// Should only be used to indicate that the app is about to crash hard.

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        void Fatal(string format, params object[] args);

 

        /// <summary>

        /// Places where we need to come back and fix something

        /// </summary>

        /// <param name="message">Helpful information</param>

        void TODO(string message);

    }

 

The logger base class

It would not take many implementations of the ILogger interface to discover that there is code that is almost always duplicated in each implementation.  Most of the code is determining if a logger is enabled and formatting strings.  Outside of that, it is calling the method that will actually write the text to a log, this is a classic example of where an abstract class is helpful.

 

    /// <summary>

    /// The abstract base class common to all types of loggers

    /// </summary>

    public abstract class LoggerBase : ILogger

    {

        /// <summary>

        /// Initializes a new instance of the LoggerBase class

        /// </summary>

        /// <param name="flags">The flags that indicate which category of log messages should be displayed</param>

        protected LoggerBase(LogFlag flags)

        {

            this.Flags = flags;

        }

 

        /// <summary>

        /// Gets the flags that indicate which category of log messages should be displayed

        /// </summary>

        protected LogFlag Flags { get; private set; }

 

        /// <summary>

        /// Debug messages should be used for a noisy spew of data that would help figure out specific problems

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        public void Debug(string message)

        {

            if (this.Active(LogFlag.Debug))

            {

                this.WriteLog(LogFlag.Debug, message);

            }

        }

 

        /// <summary>

        /// Debug messages should be used for a noisy spew of data that would help figure out specific problems

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        public void Debug(string format, params object[] args)

        {

            this.Debug(string.Format(format, args));

        }

 

        /// <summary>

        /// Trace messages should be used to indicate when you have entered or left a section of code

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        public void Trace(string message)

        {

            if (this.Active(LogFlag.Trace))

            {

                this.WriteLog(LogFlag.Trace, message);

            }

        }

 

        /// <summary>

        /// Trace messages should be used to indicate when you have entered or left a section of code

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        public void Trace(string format, params object[] args)

        {

            this.Trace(string.Format(format, args));

        }

 

        /// <summary>

        /// Info messages provide minor details - the results of specific actions, etc.

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        public void Info(string message)

        {

            if (this.Active(LogFlag.Info))

            {

                this.WriteLog(LogFlag.Info, message);

            }

        }

 

        /// <summary>

        /// Info messages provide minor details - the results of specific actions, etc.

        /// </summary>

        /// <param name="format">The format of your message, same as String.Format</param>

        /// <param name="args">The values to go into the format</param>

        public void Info(string format, params object[] args)

        {

            this.Info(string.Format(format, args));

        }

 

        /// <summary>

        /// Should only be used to indicate that the app is about to crash hard.

        /// </summary>

        /// <param name="message">Whatever you want to say</param>

        public void Fatal(string message)

        {

            this.WriteLog(LogFlag.Fatal, message);

        }

 

        /// <summary>

        /// Should only be used to indicate that the app is about to crash hard.

        /// </summary>

        /// <param name="format">The format to display the information in, same as string.Format</param>

        /// <param name="args">The information to display</param>

        public void Fatal(string format, params object[] args)

        {

            this.Fatal(string.Format(format, args));

        }

 

        /// <summary>

        /// Places where we need to come back and fix something

        /// </summary>

        /// <param name="message">Helpful information</param>

        public void TODO(string message)

        {

            if (this.Active(LogFlag.TODO))

            {

                this.WriteLog(LogFlag.TODO, message);

            }

        }

 

        /// <summary>

        /// Determine if we should write to the log

        /// </summary>

        /// <param name="flag">The specified output flad</param>

        /// <returns>If true, write to the log</returns>

        protected bool Active(LogFlag flag)

        {

            return flag == (this.Flags & flag);

        }

 

        /// <summary>

        /// The method that decides how the log file is written

        /// </summary>

        /// <param name="flag">The logflag that is enabled to allow this message to be written</param>

        /// <param name="message">The message to write</param>

        protected abstract void WriteLog(LogFlag flag, string message);

    }

 

DebugLogger

With the interface and the base class, implementing specific kinds of loggers become easy.  For example, this logger will output to the debug output window in VS2010.  Creating other classes of loggers requires an implementation of the WriteLog method which can go anywhere, a file, a webservice, a database, or whatever the problem dictates.

    /// <summary>

    /// The debug logger writes to the VS2010 debug window

    /// </summary>

    public class DebugLogger : LoggerBase

    {

        /// <summary>

        /// Initializes a new instance of the DebugLogger class.

        /// </summary>

        /// <param name="flags">Which flags are enabled for output</param>

        public DebugLogger(LogFlag flags)

            : base(flags)

        {

        }

 

        /// <summary>

        /// Write the log information to the debugger

        /// </summary>

        /// <param name="flag">The logflag that is enabled to allow this message to be written</param>

        /// <param name="message">The message to write</param>

        protected override void WriteLog(LogFlag flag, string message)

        {

            System.Diagnostics.Debug.WriteLine(message, flag.ToString());

        }

    }

LogFlags

The most important thing about these flags is that they are bits, allowing for any combination of them to be turned on or off.

  

    [Flags]

    public enum LogFlag

    {

        /// <summary>

        /// Show Fatal Messages.  These are unhandled exceptions

        /// or anything that would amount to a bug.

        /// This can't be turned off

        /// </summary>

        Fatal = 0,

 

        /// <summary>

        /// Show Debug messages, this are very verbose and contain important information

        /// when trying to track down a problem

        /// </summary>

        Debug = 1,

 

        /// <summary>

        /// Show Info Messages.  These aren't very verbose and are only meant to give

        /// a brief insight as to what is going on in the system.

        /// </summary>

        Info = 2,

 

        /// <summary>

        /// Show Trace messages.  Use these to track entry / exit from methods when you

        /// want to see what is getting called.  They don't need to be on all methods,

        /// only the methods that are interesting to you.

        /// </summary>

        Trace = 4,

 

        /// <summary>

        /// A reminder to come back and do something. 

        /// Many times you need to do something that incurs some level of technical debt.

        /// Put a TODO log entry where the code lives, when you turn on TODO logging you

        /// can see what the impact of that code is.

        /// </summary>

        TODO = 8,

 

        /// <summary>

        /// Full Verbose - show all messages

        /// </summary>

        All = LogFlag.Debug | LogFlag.Info | LogFlag.Trace | LogFlag.TODO

    }

 

What is missing

The code provided will compile as it is, only an implementation to write to a debugger is provided.  What would an implementation that writes to a file look like?  How would a programmer create an instance of a logger?  How can a programmer allow for a change in the selected logger without recompiling the application?  How would a programmer allow for an implementation to be created in the future, and be able to call that implementation without recompiling? 

Those questions are intentionally unanswered; it is the understanding of the answers not the answers themselves that expose the simplicity of this logger design. 

In Conclusion

It is easy to learn and simple to extend.  Does it do everything the log4xxx loggers do?  Absolutely not, it also does not require a wiki site describing how to use it. 

It is a logger, it logs.

Views: 101

Comment

You need to be a member of TechHui to add comments!

Join TechHui

Sponsors

web design, web development, localization

© 2018   Created by Daniel Leuck.   Powered by

Badges  |  Report an Issue  |  Terms of Service