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.
SimpleLog.SetLogFile(logDir: ".\\Log", prefix: "MyLog_", writeText: false) ;
Console.WriteLine("Log is written to {0}.", SimpleLog.FileName);
SimpleLog.Info("Test logging started.");
SimpleLog.Warning("Test is a warning.");
SimpleLog.Error("Test is an error.");
try
{
DoSomething();
}
catch(Exception ex)
{
SimpleLog.Log(ex);
}
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:
SimpleLog.Info("Test logging started.");
SimpleLog.Warning("This is a warning.");
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
{
DoSomething();
}
catch(Exception ex)
{
SimpleLog.Log(ex);
}
...
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);
}
}
private static void DoSomethingElse(string fred)
{
SimpleLog.Info("Entering method. See Source which method is meant.");
try
{
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:
public static string FileName
{
get
{
return GetFileName(DateTime.Now);
}
}
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:
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:
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:
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:
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