Error Logging With Caller Info

Introduction

This article provides a brief introduction to the use of Caller Information as a means for creating an error logging utility class for your C# 5.0/.NET 4.5 applications. Caller Information is a new language feature introduced with the .NET 4.5 update and is made available through the System.Runtime.CompilerServices namespace.
 
Caller Information Attributes

There are three attributes available for use:

  • CallerMemberName
  • CallerFilePath
  • CallerLineNumber

It is important to note that the caller line number attribute points to the line where the method is called and not to the line where the exception actually occurred.  In that respect the stack trace from an exception provides better information than the caller line number attribute when used to log exceptions.
 
Error Logging with Caller Information

Attached with the download is a simple application that implements an error logging class built using the caller information attributes.  The application is a Windows Forms application with a single form and an error logging class.  The application does not do much of anything aside from throwing as many errors as you'd like to record to a log file.

Having a look at the Solution Explorer shows that of note we have the error logging class (ErrorLogger.cs), the one and only form (Form1.cs), and the app.config file.

Error-Logging-with-CallerInfo-1.jpg

 Figure 1 - Solution Explorer
 
We will open the ErrorLogger class first and have a look. The first thing worthy of note is that we are referencing the System.Runtime.CompilerServices library that gives us access to the Caller Attributes.

The ErrorLogger class is defined as a static class so we can write directly to the error log without creating an instance of the class.  It contains only one method, WriteToErrorLog that we are really only going to pass two arguments into; the other arguments are defined as optional with a default value given.  We don't ever actually pass anything for these arguments; the values will be supplied when we call the method at runtime.

As for the arguments, in the demo application we pass in the message portion of the captured exception along with the stack trace for the exception.  The rest of the values will include the caller member name that tells us the method called when the exception occurred, the caller file path that tells us the path to the file containing the method, and the caller line number.  The caller line number points to the line where this method was called, not to where the actual error occurred; we can look into the stack trace to get the actual line number of the error but this at least shows us the catch block where the error was caught and handled.

The code is commented and fairly self-explanatory so have a look.  The basics of it are that it fetches the path to the error log folder from the app.config, then creates the directory and file, loading it with the error information we pass to the method along with the information supplied from the caller attributes.

using System;

using System.Text;

using System.IO;

using System.Runtime.CompilerServices;

 

namespace Demo_CallerInformation

{

 

    /// <summary>

    /// Write error information to a text file - the path to the error log folder is set

    /// in the app.config file

    /// </summary>

    public static class ErrorLogger

    {

        /// <summary>

        /// Write to Error Log as Text File

        /// </summary>

        /// <param name="msg">Pass in a string or the message portion of the exception</param>

        /// <param name="stackTrace">Pass in the exception stacktrace</param>

        /// <param name="memberName">CallerMemberName</param>

        /// <param name="sourceFilePath">CallerFilePath</param>

        /// <param name="sourceLineNumber">CallerLineNumber</param>

        public static void WriteToErrorLog(string msg, string stackTrace,

            [CallerMemberName] string memberName = "",

            [CallerFilePath] string sourceFilePath = "",

            [CallerLineNumber] int sourceLineNumber = 0)

        {

            // Get the path to the error log folder

            string folderPath = Properties.Settings.Default.ErrorLogPath;

 

            // make sure the folder exists, create it if necessary

            if (!System.IO.Directory.Exists(folderPath))

                System.IO.Directory.CreateDirectory(folderPath);

 

            // put together a date string to append to the error log file name

            string DateAppendage = DateTime.Now.Month.ToString() + "_" +

                DateTime.Now.Day.ToString() + "_" + DateTime.Now.Year.ToString();

            string errLogFile = folderPath + "ErrorLog_" + DateAppendage + ".txt";

 

            // make sure the error log file exists

            if (!File.Exists(errLogFile))

            {

                FileStream fs = new FileStream(errLogFile,

                    FileMode.CreateNew, FileAccess.ReadWrite);

 

                StreamWriter s = new StreamWriter(fs);

 

                s.Close();

                fs.Close();

                fs.Dispose();

            }

 

            // Capture the passed in and caller information (optional, empty arguments there) and log it

            FileStream fs1 = new FileStream(errLogFile, FileMode.Append, FileAccess.Write);

            StreamWriter s1 = new StreamWriter(fs1);

            s1.Write("Error Log Entry: " + DateTime.Now.ToLongDateString() + Environment.NewLine);

            s1.Write("Message:  " + msg + Environment.NewLine);

            s1.Write("Caller Member Name: " + memberName + Environment.NewLine);

            s1.Write("Caller Path: " + sourceFilePath + Environment.NewLine);

            s1.Write("Caller Line Number: " + sourceLineNumber.ToString() + Environment.NewLine);

            s1.Write("Stack Trace: " + stackTrace + Environment.NewLine);

            s1.Write("==================================================" + Environment.NewLine);

            s1.Close();

            fs1.Close();

            fs1.Dispose();

        }

 

    }

}
 
Next up we will have a look at the demo application, Form1.cs in this case.  The application is a simple form with a few buttons on it.  Clicking each button will generate an error, catch it, and log it to our error log using the class we just had a look at above.  At runtime, the form appears as in Figure 2 below.


Error-Logging-with-CallerInfo-2.jpg


Figure 2 - The error generating form at runtime
 
The code for the class is pretty trivial, have a look at the following.  The class contains only the button click event handlers to generate various types of errors (and one to exit the application).  Each exception is wrapped in a try-catch block and in the catch, the error encountered is logged to the error log for later analysis.
 

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Forms;

 

namespace Demo_CallerInformation

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

 

        private void btnQuit_Click(object sender, EventArgs e)

        {

            Application.Exit();

        }

 

        private void btnError1_Click(object sender, EventArgs e)

        {

            try

            {

                // throw and exception

                int a = 4;

                int b = 6;

                int c = a + b;

                int d = 0;

 

                int result = c / d; //divide by zero error

            }

            catch (Exception ex)

            {

                // write the exception to the log, note we don't need to

                // pass in anything for the caller information related arguments

                ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace);

            }

        }

 

        private void btnError2_Click(object sender, EventArgs e)

        {

            try

            {

                throw new Exception("Type 2 Error has occurred");

            }

            catch (Exception ex)

            {

                ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace);

            }

        }

 

        private void btnError3_Click(object sender, EventArgs e)

        {

            try

            {

                throw new Exception("Type 3 Error has occurred");

            }

            catch (Exception ex)

            {

                ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace);

            }

        }

 

        private void btnError4_Click(object sender, EventArgs e)

        {

            try

            {

                throw new Exception("Type 4 Error has occurred");

            }

            catch (Exception ex)

            {

                ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace);

            }

        }

 

        private void btnError5_Click(object sender, EventArgs e)

        {

            try

            {

                throw new Exception("Type 5 Error has occurred");

            }

            catch (Exception ex)

            {

                ErrorLogger.WriteToErrorLog(ex.Message, ex.StackTrace);

            }

        }

    }

}
 
The error log is generated and updated in response to each of the logged errors.  A new error log is generated for each day so that the error log does not grow overly large over time.  Each error log entry looks pretty much like the following entries.  Starting at the top we have the date stamp for the error and the message captured from the exception.  After the message we have the three caller information attributes.  First we have the name of the method where the error occurred, next the path the file containing the method, and lastly the line number where the error was recorded (not where it was thrown).  We need to look at the stack trace to see where the error actually occurred.  Looking at the first error logged below, we can see we have a divide by zero error, it was recorded on line 41 that we can confirm by looking at the project in Visual Studio, we can see in the stack trace however that the exception was actually thrown on line 35, which is correct.  For that reason, I think it is probably a good idea to keep a copy of the stack trace along with the information obtained from the caller information.  Now if we were logging events other than errors and we wanted to make entries from various locations where no error occurred then the line number supplied is great, if we are tracking down bugs, it is not as immediately helpful.

Error Log Entry: 10 July 2013

Message:  Attempted to divide by zero.

Caller Member Name: btnError1_Click

Caller Path: c:\Scott\2012 Projects\Demo_CallerInformation\Demo_CallerInformation\Form1.cs

Caller Line Number: 41

Stack Trace:    at Demo_CallerInformation.Form1.btnError1_Click(Object sender, EventArgs e) in c:\Scott\2012 Projects\Demo_CallerInformation\Demo_CallerInformation\Form1.cs:line 35

==================================================
Error Log Entry: 10 July 2013

Message:  Type 2 Error has occurred

Caller Member Name: btnError2_Click

Caller Path: c:\Scott\2012 Projects\Demo_CallerInformation\Demo_CallerInformation\Form1.cs

Caller Line Number: 53

Stack Trace:    at Demo_CallerInformation.Form1.btnError2_Click(Object sender, EventArgs e) in c:\Scott\2012 Projects\Demo_CallerInformation\Demo_CallerInformation\Form1.cs:line 49
 
Summary

In this article we took a look at the new caller information attributes in the context of creating an error logging class.  The same information could be used in a number of different ways to include logging non-error related activity.  It could also be used to make event log entries thought, write performance information to a database table and so forth.  Whilst the caller information is of great use, the stack trace is still of great value when logging error related information specifically because the stack trace provides the exact line where the exception was thrown whilst the caller information only records the line number where the logging method is called.