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

EasyLogger: make logging much easier

0.00/5 (No votes)
14 Dec 2015 1  
This is a light-weight tools to enable convenient JAVA-alike logging with pre-defined or tailored formats, in addition to convenient timestamping.

Introduction

Logging progresses/problems/states be a valuable part of software development for evaluating, testing and trouble-shooting purposes, this simple utility could bring quite some convenience to your coding by:

  • Logging different messages of different levels without typing too much, just like Log.e, Log.w, Log.i, Log.d and Log.v for ERROR, WARN, INFO, DEBUG and VERBOSE familiar by JAVA deveopers.
  • Writing these Log statements in the source codes once during your development, and extract different logs of any levels by simply adjusting the setting of the loggers.
  • Generating styled messages automatically to distinguish between messages of different levels.
  • Making multiple loggers working simultaneously.
  • Keeping timestamps to evaluate the during of execution and multiple queues to calculate the intervals of any operations.

Background

This tools was originally developed to guage the performance of some intensive calculation by using one Stopwatch to tag timestamps to any operations identified by different strings.

Without typing boilerplate codes like Debug.WriteLine() or CW(Console.WriteLine), then commenting/un-commenting them out afterwards, with forms of Log.x(e/w/i/d/v, similar to android developers) to tag messages with different log levels and fully customizable behaviours, this small tools saved me a lot of typing. To make this tools more handy, the same line of logging triggering multiple loggers to work independently and this goal is not hard to achieve thanks to the mechanism behind of Events. Finally, as originally intended, with a single StopWatch, some lists identified by strings could be used to record time/interval information that would otherwise means quite some redundant codes.

This article tries to explain how to use this tools, and also explains some techniques used to implement these desirable functions.

General Infomation

There are only 3 files included in the toolkit library: LogLevel.cs, ILog.cs and Log.cs.

Just to follow the conventions in JAVA world, the ENUM LogLevel is defined as below:

[Flags]
public enum LogLevel
{
    NONE = 0,

    // Priority constant for the println method; use Log.v.
    VERBOSE = 1,

    // Priority constant for the println method; use Log.d.
    DEBUG = 2,

    // Priority constant for the println method; use Log.i.
    INFO = 4,

    // Priority constant for the println method; use Log.w.
    WARN = 8,

    // Priority constant for the println method; use Log.e.
    ERROR = 16,

    WARN_AND_ABOVE = WARN | ERROR,
    INFO_AND_ABOVE = INFO | WARN_AND_ABOVE,
    DEBUG_AND_ABOVE = DEBUG | INFO_AND_ABOVE,
    ALL = VERBOSE | DEBUG_AND_ABOVE
}

Each logger has such a property ConcernedLogs to decide which levels of messages shall be logged: choosing predefined combinations of ALL/XXX_AND_ABOVE would achieve the effect of keeping only messages above a specific level. However, it is also possible to log only messages of specific levels. For example, by specifying ConcernedLogs as VERBOSE|ERROR, messages of DEBUG/INFO/WARN would be screened out.

The interface of ILog defines expected behaviors of the implementation as below:

public interface ILog
{
    //Hook method to define the means to record the message
    PerformLogging DoLogging { get; }

    //Min LogLevel to be recorded by calling DoLogging
    LogLevel ConcernedLogs { get; set; }

    //Hook method to uniformly generate tags of the specific LogLevels.
    Func<LogLevel, string> BuildTag { get; }

    //Hook method to convert the stacktrace of an Exception
    Func<Exception, StackTrace, string> BuildStacktrace { get; }

    //Hook method to combine TAG and DETAILS together as messages to be recorded
    Func<string, string, string> BuildFinalMessage { get; }

    //Method to log VERBOSE level details with default TAG
    void v(string details);
    //Method to log DEBUG level details with default TAG
    void d(string details);
    //Method to log INFO level details with default TAG
    void i(string details);
    //Method to log WARN level details with default TAG
    void w(string details);
    //Method to log ERROR level details with default TAG
    void e(string details);

    //Method to log VERBOSE level details with customer specific TAG
    void v(string tag, string details);
    //Method to log DEBUG level details with customer specific TAG
    void d(string tag, string details);
    //Method to log INFO level details with customer specific TAG
    void i(string tag, string details);
    //Method to log WARN level details with customer specific TAG
    void w(string tag, string details);
    //Method to log ERROR level details with customer specific TAG
    void e(string tag, string details);

    //Method to log exception
    void e(Exception exception, LogLevel? stacktraceLevel = null);
}

The PerformLogging is actually an delegate defined below, it is a function delegate and also an Event Handler:

//Delegate of general message logging handler
public delegate void PerformLogging(LogLevel logLevel, string message);

 

Unlike conventional ways depending on variables, function delegates are used to define the behaviours of the loggers. This is always my favorite to get maximum extends of flexibilities and details implementations would be covered later.

There are 3 sets of methods defined for each logger:

  1. logging functions with a single argument details: the details would be classified as some LogLevel (v(something) would be marked with VERBOSE, i(...) would be INFO, d()/w()/e() for DEBUG/WARN/ERROR respectively), then based on the ConcernedLogs, the delegates DoLogging, BuildTag and BuildFinalMessage would create tags and final output messages of correspondent LogLevel with some default template. Thus v("some message") would show differently from d("some message").
  2. For those who prefer defining some meaningful tags by themselves, the second set methods (such as void d(string tag, string details)) with two string arguments could be chosen.
  3. Exception might contain some information most useful for a developer and worth special treatment.With e(Exception exception, LogLevel? stacktraceLevel=null), the stacktrace of the exception would be converted to a string by Func<Exception, StackTrace, string> BuildStacktrace, and be saved as stacktraceLavel if it is not null.

These functions could be associated with instances only because the limitation of interface. Actaully, static methods with capital-formed names would unleash the power of this tools:

  1. V(string details), D(), I(), W() and E() would trigger v()/d()/i()/w()/e() of all loggers respectively.
  2. V(string format, params object[] args), D(...), I(...), W(...) and E(...) would have no intance counterparts, they are equal to V(string.Format(string format, params object[] args))... This might be confusing if you expect a static version of void d(string tag, string details), because that would introduce some conflictions and personally I believe incorporating string.Format() could be more helpful.
  3. static void E(Exception ex, LogLevel? stackTraceLevel = LogLevel.INFO) would trigger all loggers to log the exception with stacktrace with source codes output when stackTraceLevel is not null.

 

Implementations

Define a Log instance with delegates

Instead of keeping many descriptive information, for efficiency consideration, sometime I would keep behaviours directly as properties. In the Log.cs, the instance fields/properties are defined as below:

        #region Properties and Hooks
        public LogLevel ConcernedLogs { get; set; }
        public PerformLogging DoLogging { get; protected set; }
        public Func<LogLevel, string> BuildTag { get; protected set; }
        public Func<string, string, string> BuildFinalMessage { get; protected set; }
        public Func<Exception, StackTrace, string> BuildStacktrace { get; protected set; }
        public string Descripiton { get; set; }
       #endregion

The ConcernedLogs, as explained, is used as a filter on the level of a message by the unique execution method:

/// <summary>
/// Centralized mechanism to create tag based on the LogLevel and compose
/// and log the final message.
/// </summary>
/// <param name="logLevel">Level of the logging request.</param>
/// <param name="details">Logging details to be reserved.</param>
protected virtual void log(LogLevel logLevel, string details)
{
    if (DoLogging != null && (logLevel & ConcernedLogs) != 0)
    {
        string tag = BuildTag(logLevel);
        switch(logLevel)
        {
            case LogLevel.VERBOSE:
                v(tag, details);
                break;
            case LogLevel.DEBUG:
                d(tag, details);
                break;
            case LogLevel.INFO:
                i(tag, details);
                break;
            case LogLevel.WARN:
                w(tag, details);
                break;
            case LogLevel.ERROR:
                e(tag, details);
                break;
            default:
                throw new NotImplementedException();
        }
    }
}

 

v(tag, details) would simply stamp VERBOSE as LogLevel of the message composed by tag and details as below, so do d()...e():

public virtual void v(string tag, string details)
{
    DoLogging(LogLevel.VERBOSE, BuildFinalMessage(tag, details));
}

 

Thus the three function delegates, BuildTag/BuildFinalMessage/DoLogging, define how tags are created, then attached with the details as final message before being saved/output respectively.

For Func<LogLevel, string> BuildTag, given a specific LogLevel, it would compose a string as Tag to mark the details with some special format. Some pre-defined static functions (FormattedTag, NoTag, ShortTag & FullTag) could be referred directly, ShortTag(LogLevel logLevel) would return "[V]"/"[D]"..."[E]" respectively for different LogLevel, while FormattedTag(LogLevel logLevel):

public static string FormattedTag(LogLevel logLevel)
{
    switch (logLevel)
    {
        case LogLevel.VERBOSE:
        case LogLevel.DEBUG:
            return string.Format("{0}[{1}]", SpaceBeforeTag, logLevel.ToString().First());
        case LogLevel.INFO:
            return string.Format("[I:{0}]", ElapsedTimeString());
        case LogLevel.WARN:
            return string.Format("*[W:{0}]", ElapsedTimeString());
        case LogLevel.ERROR:
            return string.Format("***[ERROR:{0}@{1}]", ElapsedTimeString(), DateTime.Now.ToString("HH:mm:ss"));
        default:
            throw new NotImplementedException();
    }
}

would show different messages with different leadings to enable locating messages of, for example ERRORs, quickly: you just need to see where are logs started with "***[ERROR:...".

 

For Func<string, string, string> BuildFinalMessage { get; protected set; }, it just provide a flexible way to compose tag and details together as the final message. The default implementation is shown below:

public static Func<string, string, string> DefaultFinalMessage =
    (tag, details) => string.Format("{0}{1}{2}", tag, TagMessageConnector, details);

You can provide much richer features to include extra information like time/source/item# as you wish.

 

For PerformLogging DoLogging { get; protected set; }, its signature comes from below and hints it as an Event Handler:

//Delegate of general message logging handler
public delegate void PerformLogging(LogLevel logLevel, string message);

 

Notice: the message can be regarded as Tag + Details.

It could be implemented as simple as the default one:

public static PerformLogging DefaultDoLogging = (level, msg) => Console.WriteLine(msg);

 

Or could be associated with a StringBuffer as the test sample following shows, or output in reverse order to a filestream with full access control and exception handling that cannot be described.

To put them together, with only LogLevel and DoLogging as mandatory arguments, a Log instance could be defined with this constructor:

        public Log(LogLevel concernedLogs, PerformLogging doLogging, 
            Func<LogLevel, string> buildTag=null
            , Func<Exception, StackTrace, string> buildStacktrace = null
            , Func<string, string, string> buildMessage = null)
        {
            if (doLogging == null)
                throw new ArgumentNullException("Logging means must be specified!");
            this.ConcernedLogs = concernedLogs;
            this.DoLogging = doLogging;
            this.BuildTag = buildTag ?? DefaultTag;
            this.BuildFinalMessage = buildMessage??DefaultFinalMessage;
            this.BuildStacktrace = buildStacktrace??DefaultStacktrace;
            this.Descripiton = description ?? string.Format("Log{0}", this.GetHashCode());

            OnLoggingEvent += this.log;
            OnExceptionEvent += this.logException;
        }

Then the Log instance would have a quite sound set of behaviours to handle different messages. However, creating and referring these instances could hardly bring noticeable convenience to log messages, calling static methods to trigger logging of all created Log instances is my desirable approach, and its implementation is based on Event mechanism. 

Event based logging

According to Microsoft Events TutorialEvents provide a generally useful way for objects to signal state changes that may be useful to clients of that object. Events are an important building block for creating classes that can be reused in a large number of different programs.

The event to trigger message logging is defined as static one as below:

private static event PerformLogging onLoggingEvent;
public static event PerformLogging OnLoggingEvent
{
    add
    {
        lock (DefaultDoLogging)
        {
            if (onLoggingEvent == null || !onLoggingEvent.GetInvocationList().Contains(value))
            {
                onLoggingEvent += value;
            }
        }
    }
    remove
    {
        lock (DefaultDoLogging)
        {
            if (value != null && onLoggingEvent != null && onLoggingEvent.GetInvocationList().Contains(value))
            {
                onLoggingEvent -= value;
            }
        }
    }
}

 

The event can only be raised by calling either of the two private raiseEvent() methods:

private static void raiseEvent(LogLevel level, string details)
{
    try
    {
        if (onLoggingEvent != null)
            onLoggingEvent(level, details);
    }
    catch (Exception ex)
    {
        //Assume the default log is still working
        try
        {
            Instance.e(ex, LogLevel.INFO);
        }
        catch { }
    }
}

private static void raiseEvent(LogLevel level, string format, params object[] args)
{
    try
    {
        if (format == null)
            throw new ArgumentNullException("format cannot be null!");
        if (onLoggingEvent != null)
        {
            string details = string.Format(format, args);
            onLoggingEvent(level, details);
        }
    }
    catch (Exception ex)
    {
        //Assume the default log is still working
        try
        {
            Instance.e(ex, LogLevel.INFO);
        }
        catch { }
    }
}

And they are called only by the capital named static methods, to log VERBOSE messages for example:

public static void V(string details)
{
    raiseEvent(LogLevel.VERBOSE, details);
}

public static void V(string format, params object[] args)
{
    raiseEvent(LogLevel.VERBOSE, format, args);
}

Because the constructor of the Log instance hook the this.log up to the static events:

OnLoggingEvent += this.log;
OnExceptionEvent += this.logException;

 

Using "Log.V(...)" or "Log.D(...)" would trigger the OnLoggingEvent and all registered log would then call the relavent "this.v()" and "this.d()" respectively. As a result, it is fully possible to define multiple Log instances and calling these static methods would make them working as if their instance methods are called recursively. When logging is no longer needed, just un-register the event handlers would leave the source codes untouched. (Although I have included codes to unhook these events when Log instance is disposed, there is no tests to validate that).

For example, define an extended Log class like this:

public class MemLog : Log
{
    StringBuilder sb = new StringBuilder();

    public MemLog(LogLevel minLogLevel,
        Func<LogLevel, string> buildTag = null
        , Func<Exception, StackTrace, string> buildStacktrace = null
        , Func<string, string, string> buildMessage = null)
    {
        this.ConcernedLogs = minLogLevel;
        this.DoLogging = (level, msg) => sb.Insert(0, msg + "\r\n");
        this.BuildTag = buildTag ?? DefaultTag;
        this.BuildFinalMessage = buildMessage ?? DefaultFinalMessage;
        this.BuildStacktrace = buildStacktrace;

        OnLoggingEvent += this.log;
        OnExceptionEvent += this.logException;
    }

    public string GetLog()
    {
        return sb.ToString();
    }
}

A simple test method:

[TestMethod]
public void TestMethod3()
{
    Log.I("Change the DefaultLogLevel to " + Log.ChangeDefaultLogLevel(LogLevel.ALL));
    var memLog = new MemLog(LogLevel.VERBOSE | LogLevel.WARN,
        Log.FullTag,
        Log.DebuggableStacktrace
        );
    t1(3);

    Console.WriteLine("\r\n------------------------------------------");
    Console.WriteLine("MemLog shows:");
    Console.WriteLine(memLog.GetLog());
}

Would get output like this:

Result StandardOutput:    
[I:038ms]: Change the DefaultLogLevel to DEBUG_AND_ABOVE
  [D]: t1
[I:039ms]: v=3
  [V]: verbose from t3
***[ERROR:039ms@23:07:10]: To throw Exception in t3
***[ERROR:040ms@23:07:10]: Exception of type 'System.Exception' was thrown.
*[W:040ms]: StackTrace
  t3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 55
    t2: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 45
      t1: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 34
        TestMethod3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 110

  [V]: t1 done!

------------------------------------------
MemLog shows:
[VERBOSE]: t1 done!
[WARN]: StackTrace
  t3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 55
    t2: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 45
      t1: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 34
        TestMethod3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 110

[VERBOSE]: verbose from t3

The Memlog would keep only VERBOSE and WARN messages in reversed order of the default Log.

Logging of Exceptions

Exceptions could be the most valuable source to trouble-shooting problems, thus in addition to log messages as ERROR level with Log.E() or somelog.e(), the ILog interface declares a method to handle Exception:

public virtual void e(Exception exception, LogLevel? stacktraceLevel = null)
{
    logException(exception, new StackTrace(true), stacktraceLevel);
}

Similar to this.log(), the logException() method would do some pre-processing before calling log():

protected virtual void logException(Exception exception, StackTrace stacktrace, LogLevel? stacktraceLevel = null)
{
    if(exception == null || DoLogging == null)
        return;

    log(LogLevel.ERROR, exception.Message);

    if (!stacktraceLevel.HasValue || BuildStacktrace == null)
        return;
    LogLevel level = stacktraceLevel.Value;
    if ((level & ConcernedLogs) == 0)
        return;
    string stacktraceString = BuildStacktrace(exception, stacktrace);
    log(level, "StackTrace\r\n" + stacktraceString);
}

The  Func<Exception, StackTrace, string> BuildStacktrace { get; protected set; } means to extract Stacktrace information from the Exception being thrown, the given Stacktrace as a string to be output as LogLevel specified by stacktraceLevel. Uncomfortable with the Microsoft default stacktrace information that might invloves dozens of calling to underlying library, the following method would be used as default to logging only debuggable codes as the output of previous example demonstrated.

public static string DebuggableStacktrace(Exception exception, StackTrace stacktrace)
{
    if (stacktrace == null)
        throw new ArgumentNullException();
    StringBuilder sb = new StringBuilder();
    StackFrame[] frames = stacktrace.GetFrames();
    int i = 0;
    foreach(StackFrame frame in frames)
    {
        string fileName = frame.GetFileName();
        if (fileName == logClassFilename)
            continue;

        int lineNumber = frame.GetFileLineNumber();
        if(lineNumber > 0)
        {
            sb.AppendFormat("{0}{1}: {2}, line {3}\r\n", new string(' ', (++i)*2), frame.GetMethod().Name, fileName, lineNumber);
        }
    }
    return sb.ToString();
}

The logClassFilename is used to keep the filename of the Log class in the static constructor:

protected static string logClassFilename = null;
static Log()
{
    RestartStopwatch();

    StackTrace stackTrace = new StackTrace(true);
    StackFrame thisFrame = stackTrace.GetFrame(0);
    logClassFilename = thisFrame.GetFileName();
}

To support Event based Exception logging, a similar set of delegate, event and static method are defined as below:

//Delegate of Exception logging handler
public delegate void PeformExceptionLogging(Exception exception, StackTrace stacktrace, LogLevel? stacktraceLevel = null);

    private static event PeformExceptionLogging onExceptionEvent;
    public static event PeformExceptionLogging OnExceptionEvent {add{...} remove{...}}

    private static void raiseEvent(Exception exception, StackTrace stacktrace, LogLevel? stacktraceLevel = null)
    {
        try
        {
            if (onExceptionEvent != null)
            {
                onExceptionEvent(exception, stacktrace, stacktraceLevel);
            }
        }
        catch (Exception ex)
        {
            //Assume the default log is still working
            try
            {
                Instance.e(ex, LogLevel.INFO);
            }
            catch { }
        }
    }

    public static void E(Exception ex, LogLevel? stackTraceLevel = LogLevel.INFO)
    {
        raiseEvent(ex, new StackTrace(true), stackTraceLevel);
    }


Stopwatch for Everything

As mentioned earlier, this tools was developed to keep and measure time elaspsed when I tried to use a single Stopwatch instance to masure multiple operations by keeping the ticks into a list of longs referred by a string key.

private static Stopwatch stopwatch = new Stopwatch();

protected static long createMoment = DateTime.Now.Ticks;
protected static Dictionary<string, List<long>> MomentsRepository = new Dictionary<string, List<long>>();

public static ICollection<string> GetMomentIds()
{
    return MomentsRepository.Keys;
}

public static int MarkMoment(string momentId = null)
{
    if (momentId == null)
    {
        StackTrace stacktrace = new StackTrace(true);
        StackFrame callerFrame = stacktrace.GetFrames().FirstOrDefault(frame =>
            frame.GetFileName() != logClassFilename && frame.GetFileLineNumber() != 0);
        momentId = callerFrame == null ? "Unknown"
            : string.Format("{0}: {1}_L{2}", callerFrame.GetFileName(), callerFrame.GetMethod().Name, callerFrame.GetFileLineNumber());
    }
    if (!MomentsRepository.ContainsKey(momentId))
    {
        MomentsRepository.Add(momentId, new List<long>());
    }
    long ticks = DateTime.Now.Ticks;
    List<long> moments = MomentsRepository[momentId];
    moments.Add(ticks);
    return moments.Count;
}

Then it is convenient to get the logged moment and the elapsed time between these moments as below:

public static long[] GetMoments(string momentId, IEnumerable<int> indexes=null)
{
    if (momentId == null)
        throw new ArgumentNullException("momentId cannot be null to retrieve the concerned moments.");
    //Returns empty array if no such moments recorded
    if (!MomentsRepository.ContainsKey(momentId))
        return new long[0];

    if (indexes == null)
        return MomentsRepository[momentId].ToArray();

    var moments = MomentsRepository[momentId];
    List<int> qualifiedIndexes = indexes.Where(i => i >= 0 && i < moments.Count).ToList();
    var result = qualifiedIndexes.Select(i => moments[i]).ToArray();
    return result;
}

public static long[] GetIntervals(string momentId, IEnumerable<int> indexes=null)
{
    if (momentId == null)
        throw new ArgumentNullException("momentId cannot be null to retrieve the concerned moments.");
    //Returns empty array if no such moments recorded
    if (!MomentsRepository.ContainsKey(momentId))
        return new long[0];

    var moments = MomentsRepository[momentId];
    if (indexes == null)
    {
        var intervals = moments.Skip(1).Select((v, index) => v - moments[index-1]).ToArray();
        return intervals;
    }
    List<int> qualifiedIndexes = indexes.Where(i => i > 0 && i < moments.Count).ToList();

    long[] selectedIntervals = qualifiedIndexes.Select(i => moments[i] - moments[i-1]).ToArray();
    return selectedIntervals;
}

Alternatively, logging the entry and exit moments as two lists with two string keys might be easier for post-processing.

Using the code

There is a test project attached with the source codes to show how to use this tools.

For example, the MemLog.cs shows an extension of the Log class as below:

public class MemLog : Log
{
    StringBuilder sb = new StringBuilder();

    public MemLog(LogLevel minLogLevel,
        Func<LogLevel, string> buildTag = null
        , Func<Exception, StackTrace, string> buildStacktrace = null
        , Func<string, string, string> buildMessage = null)
    {
        this.ConcernedLogs = minLogLevel;
        this.DoLogging = (level, msg) => sb.Insert(0, msg + "\r\n");
        this.BuildTag = buildTag ?? DefaultTag;
        this.BuildFinalMessage = buildMessage ?? DefaultFinalMessage;
        this.BuildStacktrace = buildStacktrace;

        OnLoggingEvent += this.log;
        OnExceptionEvent += this.logException;
    }

    public string GetLog()
    {
        return sb.ToString();
    }
}

Then the test method declare one instance and all calling to the static methods of Log.cs would leave VERBOSE and WARN messages in the StringBuild in reversed order:

[TestMethod]
public void TestMethod3()
{
    Log.I("Change the DefaultLogLevel to " + Log.ChangeDefaultLogLevel(LogLevel.ALL));
    var memLog = new MemLog(LogLevel.VERBOSE | LogLevel.WARN,
        Log.FullTag,
        Log.DebuggableStacktrace
        );
    t1(3);

    Console.WriteLine("\r\n------------------------------------------");
    Console.WriteLine("MemLog shows:");
    Console.WriteLine(memLog.GetLog());
}

And the output of the test would be:

Test Name:    TestMethod3
Test Outcome:    Passed
Result StandardOutput:    
[I:000ms]: Restart stopwatch at 12/14/2015 11:59:08 PM, with time elapsed of 000ms
[I:006ms]: Change the DefaultLogLevel to DEBUG_AND_ABOVE
  [D]: t1
[I:007ms]: v=3
  [V]: verbose from t3
***[ERROR:008ms@23:59:08]: To throw Exception in t3
***[ERROR:009ms@23:59:08]: Exception of type 'System.Exception' was thrown.
*[W:009ms]: StackTrace
  t3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 55
    t2: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 45
      t1: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 34
        TestMethod3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 110

  [V]: t1 done!

------------------------------------------
MemLog shows:
[VERBOSE]: t1 done!
[WARN]: StackTrace
  t3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 55
    t2: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 45
      t1: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 34
        TestMethod3: d:\Trial\EasyLogger\EasyLoggerTest\UnitTest1.cs, line 110

[VERBOSE]: verbose from t3

It is easy to see how convenient it could be to log messages in another form without much modification on the source codes.

The latest source codes, if there is any update, could also be found from https://github.com/Cruisoring/EasyLog/tree/master/EasyLogger.

 

History

15/12/2015: First release.

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