Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simple Log

0.00/5 (No votes)
2 Feb 2021 4  
A simple but robust logging class
In this tip, you will see how to do essential logging in one simple, robust, static, thread-safe class. No initialization, no configuration, no dependencies. Don't think, just log.
// Log to a sub-directory 'Log' of the current working directory.
// Prefix log file with 'MyLog_'.
// Write XML file, not plain text.
// This is an optional call and has only to be done once,
// pereferably before the first log entry is written.
SimpleLog.SetLogFile(logDir: ".\\Log", prefix: "MyLog_", writeText: false) ;

// Show log file path
Console.WriteLine("Log is written to {0}.", SimpleLog.FileName);

// Write info message to log
SimpleLog.Info("Test logging started.");

// Write warning message to log
SimpleLog.Warning("Test is a warning.");

// Write error message to log
SimpleLog.Error("Test is an error.");

try
{
    // For demonstration, do logging in sub-method,
    // throw an exception, catch it, wrap it in
    // another exception and throw it.
    DoSomething();
}
catch(Exception ex)
{
    // Write exception with all inner exceptions to log
    SimpleLog.Log(ex);
}

// Show log file in browser
SimpleLog.ShowLogFile();

Introduction

A simple but robust logging class that fulfills the following requirements:

  • One simple, static class, easy to understand
  • Works "out of the box," just include it and start logging. No initialization, no configuration necessary. No dependencies
  • Simple, static methods to write a log entry
  • Logs XML fragments or plain text to a file, one tag or one line per log entry
  • Easy but flexible way to change file and folder where logs are written to
  • Possibility to pass log severity (info, warning, error, exception)
  • Possibility of simple filtering by log severity
  • Log exceptions recursively with all inner exceptions, data, stack trace and specific properties of some exception types
  • Has very little impact on performance of the main program due to queue-and-background task approach
  • Is thread-safe, i.e., several threads can log into the same file concurrently
  • Automatically logs the class and method name where the log method was called from
  • Exceptions occurring when writing to the log file, e.g., due to insufficient rights, get logged to event log, including log file name and current process user name. Log source is "SimpleLog".
  • Robust, compact and well-documented

Background

Some time ago, I was looking for a really simple logging class.

Having a small project with a handful of files and classes, I didn't want to blow it up with a complex framework like log4net with dozens of classes and files. I didn't want to deal with hundreds of options and configuration settings. I didn't want to add a configuration file, just for logging. I didn't want to spend hours learning how logging works with a particular framework.

I just wanted to include one class as source code and start logging. A class I can understand in minutes, not days, and I can eventually extend to my needs. Simple as that. I thought "why re-invent the wheel?" there are millions of logging approaches out there!

I stumbled over Marco Manso's Really Simple Log Writer. Yeah, that is really simple! A static class with some logging methods, working "out of the box". Logs XML fragments to a file, one tag per entry. Just what I was looking for!

Well ... logging inner exceptions should be done recursively. No big deal, changed in a minute. Oh, there is some code-redundancy in writing the logs ... no problem, duplicate code extracted in a separate method. Hmm, configuring the log file name and its location could be done in a more consistent, flexible way ... and comments and regions would be a good idea, too. When I'm logging, I'd like to distinguish between info, warning and error. That's really not asking for too much, is it? Ok, logging severity built in, along with some shortcut methods, of course. For compactness, IMHO, severity, date and time, should not be logged as separate tags, but as attributes - changed. One thing that I ever wanted and often missed, is that the location in code where a logging method was called is logged automatically. This way, you do not have to write "exception in method xy ..." each time; logging does that for you! Built in quickly. What do people suggest for Marco's class? A method to get the log file completed to a regular XML document? Right, that will be needed for sure. And when we're already about it, it could be shown in the browser with just one call. Great, isn't it? Log filtering is something that is always available in every logging system. Shall this class provide it, too? Yes, but it must be very basic and intuitive. Should not have an impact on the learning curve - built in a simple filtering by severity that everyone will understand instantly. Whew, we're through now, aren't we? What about performance and thread safety? True. We're neither - nor. Good logging systems have a queue and a background task to actually write the entries to disk. That way, they achieve both, performance and thread safety. Hmm, wasn't I looking for a simple class initially? That feature would make the class much (?) more complex. But it's the correct way how to do it! It's the last thing I'm adding.

Now, the class can be used in any project without worrying - and it is still comparably simple and easy to understand. From my point of view, it should be a good compromise between simplicity and functionality.

Using the Code

As Marco nicely expressed it: That's the beauty of it. Just include the class as source code anywhere in your project and start logging:

// Write info message to log
SimpleLog.Info("Test logging started.");

// Write warning message to log
SimpleLog.Warning("This is a warning.");

// Write error message to log
SimpleLog.Error("This is an error.");

That will produce the following output in a log file e.g. "2013_04_29.log" in the current working directory:

<LogEntry Date="2013-04-29 18:56:43" Severity="Info" 
              Source="SimpleLogDemo.Program.Main" ThreadId="9">
  <Message>Test logging started.</Message>
</LogEntry>
<LogEntry Date="2013-04-29 18:56:43" Severity="Warning" 
               Source="SimpleLogDemo.Program.Main" ThreadId="9">
  <Message>This is a warning.</Message>
</LogEntry>
<LogEntry Date="2013-04-29 18:56:43" Severity="Warning" 
               Source="SimpleLogDemo.Program.Main" ThreadId="9">
  <Message>This is an error.</Message>
</LogEntry>

Of course, exceptions can be logged, too, including all inner exceptions:

try
{
    // For demonstration, do logging in sub-method, 
    // throw an exception, catch it, wrap it in 
    // another exception and throw it.
    DoSomething();
}
catch(Exception ex)
{
    // Write exception with all inner exceptions to log
    SimpleLog.Log(ex);
}

...

/// <summary>
/// Do something for demonstration
/// </summary>
private static void DoSomething()
{
    SimpleLog.Info("Entering method. See Source which method is meant.");

    try
    {
        DoSomethingElse(null);
    }
    catch(Exception ex)
    {
        throw new InvalidOperationException("Something went wrong.", ex);
    }
}

/// <summary>
/// Do something else for demonstration
/// </summary>
/// <remarks>
/// Purposely provoke an exception
/// </remarks>
/// <param name="fred">Should not be null</param>
private static void DoSomethingElse(string fred)
{
    SimpleLog.Info("Entering method. See Source which method is meant.");

    try
    {
        // Purposely provoking an exception.
        int a = fred.IndexOf("Hello");
    }
    catch(Exception ex)
    {
        throw new Exception("Something went wrong.", ex);
    }
}

That will produce the following output:

<LogEntry Date="2013-04-29 18:56:43" Severity="Info" 
                 Source="SimpleLogDemo.Program.DoSomething" ThreadId="9">
  <Message>Entering method. See Source which method is meant.</Message>
</LogEntry>
<LogEntry Date="2013-04-29 18:56:43" Severity="Info" 
           Source="SimpleLogDemo.Program.DoSomethingElse" ThreadId="9">
  <Message>Entering method. See Source which method is meant.</Message>
</LogEntry>
<LogEntry Date="2013-04-29 18:56:43" Severity="Exception" 
             Source="SimpleLogDemo.Program.Main" ThreadId="9">
  <Exception Type="System.InvalidOperationException" 
  Source="SimpleLogDemo.Program.DoSomething">
    <Message>Something went wrong.</Message>
    <Exception Type="System.Exception" 
    Source="SimpleLogDemo.Program.DoSomethingElse">
      <Message>Something went wrong.</Message>
      <Exception Type="System.NullReferenceException" 
      Source="SimpleLogDemo.Program.DoSomethingElse">
        <Message>Object reference not set to an instance of an object.</Message>
        <StackTrace>   at SimpleLogDemo.Program.DoSomethingElse(String fred) 
           in D:\Projekt\VisualStudio\SimpleLogDemo\SimpleLogDemo\Program.cs:line 91
        </StackTrace>
      </Exception>
    </Exception>
  </Exception>
</LogEntry>

The log file that is written to is assembled as follows:

/// <summary>
/// File to log in
/// </summary>
/// <remarks>
/// Is assembled from <see cref="LogDir"/>, <see cref="Prefix"/>, the current date and 
/// time formatted in <see cref="DateFormat"/>,
/// <see cref="Suffix"/>, "." and <see cref="Extension"/>. 
/// So, by default, the file is named e.g. "2013_04_21.log" and is written to the current 
/// working directory.
/// It is assembled in <see cref="GetFileName"/> 
/// using <code>string.Format("{0}\\{1}{2}{3}.{4}", 
/// LogDir, Prefix, dateTime.ToString(DateFormat), Suffix, Extension)</code>.
/// </remarks>
public static string FileName
{
    get
    {
        return GetFileName(DateTime.Now);
    }
}

/// <summary>
/// Gets the log filename for the passed date
/// </summary>
/// <param name="dateTime">The date to get the log file name for</param>
/// <returns>The log filename for the passed date</returns>
public static string GetFileName(DateTime dateTime)
{
    return string.Format("{0}\\{1}{2}{3}.{4}", LogDir, Prefix, 
                         dateTime.ToString(DateFormat), Suffix, Extension);
}

Want to use another log file at another location? Apart from setting several properties like prefix, suffix, extension, directory, log level, etc. separately, you can use convenience method SetLogFile to set them all at once:

/// <summary>
/// Set all log properties at once
/// </summary>
/// <remarks>
/// Set all log customizing properties at once. This is a pure convenience function. 
/// All parameters are optional.
/// When <see cref="logDir"/> is set and it cannot be created or writing a first entry fails, 
/// no exception is thrown, but the previous directory,
/// respectively the default directory (the current working directory), is used instead.
/// </remarks>
/// <param name="logDir"><see cref="LogDir"/> 
/// for details. When null is passed here, 
/// <see cref="LogDir"/> is not set. 
/// Here, <see cref="LogDir"/> is created, when it does not exist.</param>
/// <param name="prefix"><see cref="Prefix"/> for details. 
/// When null is passed here, <see cref="Prefix"/> is not set.</param>
/// <param name="suffix"><see cref="Suffix"/> for details. 
/// When null is passed here, <see cref="Suffix"/> is not set.</param>
/// <param name="extension"><see cref="Extension"/> for details. 
/// When null is passed here, <see cref="Extension"/> is not set.</param>
/// <param name="dateFormat"><see cref="DateFormat"/> for details. 
/// When null is passed here, <see cref="DateFormat"/> is not set.</param>
/// <param name="logLevel"><see cref="LogLevel"/> for details. 
/// When null is passed here, <see cref="LogLevel"/> is not set.</param>
/// <param name="startExplicitly"><see cref="StartExplicitly"/> for details. 
/// When null is passed here, <see cref="StartExplicitly"/> is not set.</param>
/// <param name="check">Whether to call <see cref="Check"/>, 
/// i.e. whether to write a test entry after setting the new log file. 
/// If true, the result of <see cref="Check"/> is returned.</param>
/// <param name="writeText"><see cref="WriteText"/> for details. 
/// When null is passed here, <see cref="WriteText"/> is not set.</param>
/// <param name="textSeparator"><see cref="TextSeparator"/> for details. 
/// When null is passed here, <see cref="TextSeparator"/> is not set.</param>
/// <returns>Null on success, otherwise an exception with what went wrong.</returns>
public static Exception SetLogFile(string logDir = null, 
                                   string prefix = null, string suffix = null, 
string extension = null, string dateFormat = null, Severity? logLevel = null, 
bool? startExplicitly = null, bool check = true, bool? writeText = null, 
string textSeparator = null)

For example, you can say:

// Log to a sub-directory 'Log' of the current working directory.
// Prefix log file with 'MyLog_'.
// Write XML file, not plain text.
// This is an optional call and has only to be done once,
// preferably before the first log entry is written.
SimpleLog.SetLogFile(logDir: ".\\Log", prefix: "MyLog_", writeText: false);

Note: In contrast to logging itself, changing log file settings, i.e., changing the file it is written to, is NOT thread-safe. It won't crash, though, but unwanted results may occur. It is not intended to change log file settings during regular logging, but once at application startup (before the first log entry is written) and then left alone.

To get a log file as complete XML, you can use:

/// <summary>
/// Get the log file for the passed date as XML document
/// </summary>
/// <remarks>
/// Does not throw an exception when the log file does not exist.
/// </remarks>
/// <param name="dateTime">The date and time to get the log file for. 
/// Use DateTime.Now to get the current log file.</param>
/// <returns>The log file as XML document or null when it does not exist.</returns>
public static XDocument GetLogFileAsXml(DateTime dateTime)

You can also show the current log file in the related application (e.g., a browser or a text editor) with just one line:

/// <summary>
/// Shows the current log file
/// </summary>
/// <remarks>
/// Opens the default program to show text or XML files and displays 
/// the requested file, if it exists. Does nothing otherwise.
/// When <see cref="WriteText"/> is false, a temporary XML file 
/// is created and saved in the users's temporary path each time this method is called.
/// So don't use it excessively in that case. Otherwise, the log file itself is shown.
/// </remarks>
public static void ShowLogFile()

That's it! You don't have to care about anything else. Enjoy!

History

  • 29th April, 2013 - First published, Version 1.0
  • 19th June, 2014 - Added possibility to log directly to disk, without background task, Version 1.1
  • 19th June, 2014 - Added possibility to start background task explicitly
  • 7th December, 2014 - Corrected typo and added stub for writing text instead of XML, Version 1.1.1
  • 8th December, 2014 - Basic implementation of writing text instead of XML, Version 1.2
  • 18th June, 2016 - Fixed bug that avoided that last remaining log entries get logged, Version 1.2.1
  • 16th July, 2016 - Fixed bug when transforming exception data to XML attributes, Version 1.2.2
  • 16th July, 2016 - Write exceptions occurring when writing to the log file to event log, Version 1.2.2
  • 3rd February, 2021 - Using Path.DirectorySeparatorChar when building log path, Version 1.3.0
  • 3rd February, 2021 - Write log files in unicode (UTF8), Version 1.3.0
  • 3rd February, 2021 - Avoid log running full by repeated log entries, Version 1.3.0
  • 3rd February, 2021 - Avoid possible exception when ex.Source is null, Version 1.3.0
  • 3rd February, 2021 - Upgraded demo to .NET 4.7.2, Version 1.3.0

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here