Click here to Skip to main content
16,020,677 members
Articles / Programming Languages / C#

TracerX Logger and Viewer for .NET

Rate me:
Please Sign up or sign in to vote.
4.83/5 (46 votes)
11 Apr 2023MIT31 min read 240.1K   4.3K   242   105
Easy to use logger with a powerful viewer that supports filtering by thread, logger, etc.

Introduction 

The TracerX logger is easy to get started with, yet has advanced features for .NET applications. It can send output to multiple destinations (circular file, event log, etc.). It can generate text and/or binary files.

It has a powerful viewer that supports filtering and coloring by threads, category, trace level, and more. The viewer can also collapse and expand the output from each method call, show absolute or relative timestamps, and view/walk the call stack leading to any line of output. In my experience, these features make diagnosing problems much easier than using a plain text editor.

This article explains how to use TracerX, and includes some inline code samples.

TracerX is a C# Visual Studio 2019 project (targeting .NET 3.5, .NET 4.0, .NET 4.6, and .NET Core). It is used by many commercial products.

The logger component is available as a NuGet package (just search for TracerX).

Source code and binaries for the logger, viewer, and service (optional but great for remote access) are available on GitHub (MarkLTX/TracerX (github.com)).

Table of Contents

The TracerX Viewer

This article is mostly about how to use the TracerX logger, but the viewer is a key attraction. Here's a screenshot of the TracerX log viewer:

TracerX/TracerXViewer.png

Most of the following features can be discovered by exploring the menus, except for those that are accessed by double-clicking. Don't forget to try the context menus for the rows and column headers!

  1. You can filter and/or colorize the log by thread name, thread number, logger, trace level, text wildcards, and method name.
  2. The message text is indented according to stack depth.
  3. You can collapse and expand method calls by double-clicking rows where "+" and "-" appear.
  4. You can navigate up and down the call stack using the crumb bar and/or context menu.
  5. You can click the arrows in the crumb bar to see (and jump to) the methods called at a given level.
  6. You can view absolute or relative timestamps. Any row can be the "zero time" record.
  7. You can collapse and expand rows that contain embedded newlines by double-clicking lines with yellow triangles.
  8. You can bookmark individual rows, or all rows that...
    1. contain a specified search string.
    2. are from one or more selected threads, loggers, or trace levels.
  9. You can view the call stack (in a window) leading to a selected row, and navigate to rows in the call stack.
  10. You can jump to the next block from the same thread, or the next block from a different thread.
  11. You can select rows and copy the text column or all columns to the clipboard.
  12. You can customize the columns (show/hide columns, change their order).
  13. Long messages, with or without embedded newlines, can be viewed in a resizable text window.

"Hello World" with TracerX

Here's a very simple program that generates a TracerX log file:

C#
using TracerX;

namespace HelloWorld
{
    class Program
    {
        // Create a Logger named after this class (could have any name).
        static Logger Log = Logger.GetLogger("Program");

        static void Main(string[] args)
        {
            // Open the output file using default settings.
            Logger.DefaultBinaryFile.Open();

            // Log a string.
            Log.Info("Hello, World!");
        }
    }
}

The first statement in Main() opens the output file using the default file name (derived from the executable's name) in the default directory (the user's ApplicationData directory).

The second statement logs a string at the Info trace level through the Logger named "Program".

The output appears like this in the viewer. Note that some columns are hidden, and lines 1-16 are logged automatically when the file is opened (they can be suppressed if you like).

Image 2

Logging Basics

Let's move beyond "hello world" and cover some of the underlying concepts and features of TracerX.

Loggers

TracerX uses the log4net concept of a hierarchy of loggers. All logging is done through instances of the Logger class. Each Logger has a name that appears in the "Logger" column in the viewer. Each Logger should represent a category of logging within your application such as "Reporting" or "Configuration".

You must decide how many Loggers to create and what to name them. One approach is to create a Logger for each class in your app, like this:

C#
using TracerX;

class CoolClass
{
    private static readonly Logger Log =
            Logger.GetLogger("CoolClass");

    // etc.
}

The static method Logger.GetLogger() is the only way to create Loggers in your code. It has these overloads:

C#
public static Logger GetLogger(string name)
public static Logger GetLogger(Type type)

GetLogger() creates a Logger instance with the specified name (if it doesn't already exist). The name appears under the "Logger" column in the viewer. All subsequent calls with the same name return the same instance, so code in several different files can use the same Logger, if desired.

A Type is just converted to its fully qualified name and passed on to the string overload.

The Logger Tree

Logger names can contain periods, such as "MyNamespace.CoolClass". TracerX uses the period-separated substrings of the names to create a tree of Loggers. For example, "A.B" is the parent of "A.B.Anything". Note that "A.B" need not exist for "A.B.C" to exist. It doesn't matter what order the Loggers are created in. TracerX inserts each Logger into the appropriate place in the tree when it is created.

Predefined Loggers

TracerX has the following predefined loggers (static instances of the Logger class).

Predefined Logger Description
Logger.Root The root of the Logger tree. Every other Logger (including those you create) is a descendant of the Root Logger. You can do all your logging through Logger.Root without ever calling GetLogger(), but that leaves you with no ability to filter the output by Logger name in the viewer, or to control the amount of logging done by different areas of your code at run-time.
Logger.Current

The last Logger referenced by the calling thread. Usually used in "common" code that doesn't have it's own logger, in order to use the same logger as the calling code. It is initialized to Root for each thread.

Logger.StandardData The Logger used internally by TracerX to log information about the environment, computer, and user when the log file is opened.

Trace Levels

Every message logged by TracerX has one of the following "trace levels" (from lowest to highest). These are values of the enum type TracerX.TraceLevel.

  • Fatal
  • Error
  • Warn
  • Info
  • Debug
  • Verbose

The CodeProject article "The Art of Logging" [^] contains an excellent guide to choosing the appropriate TraceLevel for each message.

How to Log a Message

Assuming you have a Logger instance named Log, you can use it to log messages with statements like this:

C#
Log.Info("A string logged at the Info level.");
Log.Debug("Several objects at Debug level ", obj1, obj2);

The trace level of the message depends on the method you call (Log.Info(), Log.Debug(), etc.).

You can pass any number of arguments to the above methods. The arguments need not be strings. If (and only if) the method's trace level is enabled, each argument is converted to a string and concatenated to form the message that appears in the viewer's "Text" column. You should avoid calling ToString() yourself since this wastes processing if the message doesn't get logged due to its trace level.

Each trace level also has a version that uses the same semantics as string.Format(), like this:

C#
Log.VerboseFormat("obj1 is {0}.", obj1);

How to Log a Method Call

Logging your method calls enables several powerful features in the viewer, such as the ability to filter/color by method name, collapse/expand the output from a given method call, and view/walk the call stack. To log calls to a given method, wrap the contents of the method in a using block like this (this assumes you've created a Logger named Log):

C#
private void YourMethod()
{
    using (Log.InfoCall())
    {
        // Other code goes here!
    }
}

As before, the trace level is implied by the name of the Logger method (e.g., Log.InfoCall(), Log.DebugCall(), etc.). If that trace level is enabled, the following occurs:

  1. TracerX obtains the name of the calling method (i.e., "YourMethod").
  2. A message like "YourMethod: entered" is logged.
  3. Indentation is increased for any logging done by the calling thread inside the "using" block.
  4. The viewer displays "YourMethod" in the Method column for any logging inside the "using" block.
  5. An object is returned whose Dispose method logs a message like "YourMethod: exiting" and restores the previous indentation and method name for the thread.

You have the option of passing the method name as a string to InfoCall(), DebugCall(), etc.  Some reasons to do that are:

  1. You plan to obfuscate your code (and you don't want the obfuscated method names in the log).
  2. You want to simulate a method call in an "if" statement, "for" loop, or other block.
  3. You want to use a "custom" method name or include additional info with the method name.

Just FYI, all calls to InfoCall(), DebugCall(), etc., return the same object. If the trace level for such a call is not enabled, the call returns null.

Note that this feature is not compatible with async/await.  It requires the entering and exiting of the "using" block to occur on the same thread.  Using the await keyword inside the "using" block violates that requirement and causes issues such as NullReferenceExceptions.

Another Code Sample

This code demonstrates a technique for initializing TracerX before Program.Main() is called. Then, it logs a few messages and method calls using different loggers.

C#
using System;
using System.Threading;
using System.IO;
using TracerX;

namespace Sample
{
    class Program
    {
        private static readonly Logger Log = Logger.GetLogger("Program");

        // Just one way to initialize TracerX early.
        private static bool LogFileOpened = InitLogging();

        // Initialize the TracerX logging system.
        private static bool InitLogging()
        {
            // It's best to name most threads.
            Thread.CurrentThread.Name = "MainThread";

            // Load TracerX configuration from an XML file.
            Logger.Xml.Configure("TracerX.xml");

            // Open the log file.
            return Logger.DefaultBinaryFile.Open();
        }

        static void Main(string[] args)
        {
            using (Log.InfoCall())
            {
                Helper.Bar();
                Helper.Foo();
            }
        }
    }

    class Helper {
        private static readonly Logger Log = Logger.GetLogger("Helper");

        public static void Foo()
        {
            using (Log.DebugCall())
            {
                Log.Debug(DateTime.Now, " is the current time.");
                Bar();
            }
        }

        public static void Bar()
        {
            using (Log.DebugCall())
            {
                Log.Debug("This object's type is ", typeof(Helper));
            }
        }
    }
}

Logging Destinations

Each call to a logging method like Logger.Info() or Logger.Debug() can send its output to any combination of six possible destinations. The destinations to which a given message is sent depends on its TraceLevel. Each destination has a property in the Logger class that specifies the maximum TraceLevel sent to that destination by the Logger instance. These properties determine which messages are sent to which destinations.

Destination Logger property Initial value in Root Logger Initial value in other Loggers
Binary file BinaryFileTraceLevel TraceLevel.Info TraceLevel.Inherited
Text file TextFileTraceLevel TraceLevel.Off TraceLevel.Inherited
Console (i.e., command window) ConsoleTraceLevel TraceLevel.Off TraceLevel.Inherited
Trace.WriteLine() DebugTraceLevel TraceLevel.Off TraceLevel.Inherited
Event log EventLogTraceLevel TraceLevel.Off TraceLevel.Inherited
Event handler EventHandlerTraceLevel TraceLevel.Off TraceLevel.Inherited

For example, suppose you've declared a Logger named Log and your code contains the following statement:

C#
Log.Info("Hello, World!");

The TraceLevel for this statement is Info. Therefore...

  • If Log.BinaryFileTraceLevel >= TraceLevel.Info, the message is sent to the binary log file.
  • If Log.TextFileTraceLevel >= TraceLevel.Info, the message is sent to the text log file.
  • If Log.ConsoleTraceLevel >= TraceLevel.Info, the message is sent to the console.
  • If Log.DebugTraceLevel >= TraceLevel.Info, the message is passed to Trace.WriteLine().
  • If Log.EventLogTraceLevel >= TraceLevel.Info, the message is sent to the application event log.
  • If Log.EventHandlerTraceLevel >= TraceLevel.Info, the Logger.MessageCreated event is raised.

Notice that the initial values of these properties for all Loggers you create is Inherited. This means the effective TraceLevel value for each property is inherited from the parent Logger, all the way up to the root Logger if necessary. Therefore, your Loggers will inherit the Root trace levels shown in the table above unless you explicitly override them, which is not usually done. Most users just set the Root levels and let all other Loggers inherit them.

Also notice that the root Logger's initial (default) TraceLevel for all destinations except the binary file is Off. This means no output will be sent to those destinations unless you change their TraceLevels. A typical scenario would be for an application to set the following levels:

C#
Logger.Root.BinaryFileTraceLevel = TraceLevel.Debug;
Logger.Root.EventLogTraceLevel =   TraceLevel.Error;

This causes any log message with a level of Debug or lower to go to the file, and those with a level of Error or lower to also go to the event log.

Regardless of what these properties are set to, no output will be sent to the binary or text files unless those files have been opened.

File Logging

TracerX supports two types of output files: binary and text. Both types include the timestamp, thread name/number, trace level, and method name with each message you log. Each Logger can write to both, either, or neither type of file, depending on the configuration.

Each Logger has a BinaryFile property that specifies the binary file used by that Logger. This property is initially equal to the static property Logger.DefaultBinaryFile, so that all Loggers share the same binary file. Therefore, when you configure and open Logger.DefaultBinaryFile, you are configuring and opening the shared binary file. You can create your own instances of BinaryFile for use by individual Loggers (or groups of Loggers).

That same pattern applies to text files as well, except that the class for text files is TextFile, the corresponding Logger instance property is TextFile, and the corresponding static property that all instances are initialized with is Logger.DefaultTextFile.

The primary advantage of using the binary file is that you can view it with the TracerX-Viewer.exe application, which has powerful filtering, searching, coloring, formatting, and navigation features. The binary file also tends to be more compact than the text file (depending on the fields you specify in the text file's format string). The disadvantage of the binary file is that you can only view it with the viewer. To get the best of both worlds, you may opt to generate both files. Then you can use the viewer if you have it, Notepad if you don't.

Maximum File Size

Both file types require you to specify a maximum file size, up to 4095 megabytes. Typically, you will specify 1 to 100 megabytes. What happens when the file(s) reach the maximum size depends on what the file's FullFilePolicy property is set to. It's an enum with the following values:

  • Wrap
  • Roll
  • Close

Circular Logging

Both file types support circular logging, which occurs when FullFilePolicy is Wrap. In circular logging, the file is divided into two parts. The first part is preserved and never lost (until the file itself is deleted). The second part is the circular part. When the log file grows to the maximum designated size, TracerX 'wraps' the file. That is, it sets the write position back to the beginning of the circular part and begins replacing the old output with the new output. This may occur many times and is completely automatic.

Circular logging is recommended for long-running applications and programs that may generate an unpredictably large amount of output per execution, such as services.

The Viewer application will automatically find the first chronological record in the circular part of a binary file and display all records in chronological order. With text files, you have to do this manually.

To enable circular logging, the configuration property FullFilePolicy must be set to Wrap, and CircularStartSizeKb and/or CircularStartDelaySeconds (described later) must not be zero.

Archiving

Both file types support archiving. Archiving occurs when a new output file is being opened and an old file with the same name already exists. TracerX "archives" (i.e., backs up) the existing file by renaming it with _01 appended to the base file name. The existing _01 file becomes the _02 file and so on up the value you specify. The oldest file gets the highest number. Files with numbers higher than the specified value are deleted. The highest value you can specify is 99.

If the FullFilePolicy is Roll when the file grows to its maximum designated size, the file is closed, archived, and reopened. This can happen many times and is similar to log4net's RollingFileAppender. When archiving is combined with appending to a text file, the output from a given execution can begin in the middle of, say, LogFile_02.txt, and end in LogFile.txt. That can be confusing.

Appending

Both file types support (optional) appending to the existing file when the log is opened, vs. archiving the file. TracerX can be configured to never append or to append when the existing file is not too large.

Configuration Properties

Each of the file classes (BinaryFile and TextFile) has several configuration properties. These are described in the following table. All of them have reasonable defaults so you can get started without much effort (remember Hello World), but you will probably want to at least set the directory in production code. For example, to set the directory where the file is created, you might code something like this:

C#
Logger.DefaultBinaryFile.Directory = "C:\\Logs";
// and/or
Logger.DefaultTextFile.Directory = "C:\\Logs";

The following properties are common to both file objects (inherited from a base class, actually). Most of these properties can't, or shouldn't, be changed after the corresponding file is opened. All but Name can be set via an XML configuration file by calling Logger.XML.Configure() before, after, or instead of setting them in code. Note that XML configuration applies only to Logger.DefaultBinaryFile and Logger.DefaultTextFile, not file objects you create yourself.

Property Description
Directory

The directory where the file is located. The default value corresponds to System.Environment.SpecialFolder.LocalApplicationData.

Environment variables are expanded when this is set. The following pseudo %variables% are also supported:

%LOCAL_APPDATA% is replaced by System.Environment.SpecialFolder.LocalApplicationData. This is the same as the default, but you can use this to specify a subdirectory such as %LOCAL_APPDATA%\Product\Logs.

%COMMON_APPDATA% is replaced by System.Environment.SpecialFolder.CommonApplicationData.

%DESKTOP% is replaced by System.Environment.SpecialFolder.Desktop.

%MY_DOCUMENTS% is replaced by System.Environment.SpecialFolder.MyDocuments.

%EXEDIR% is replaced by the directory of the executable, regardless of the current directory.

Name

The name of the file. The default value is the name of the process or AppDomain. The file extension is coerced to '.txt' for text files, and '.tx1' for binary files, regardless of what you specify, so you may as well omit the extension.

If you want the file name to end with "_00", set Use_00 to true. Don't put "_00" at the end of this string, or you'll end up with filenames like "MyLog_00_01.tx1".

AppendIfSmallerThanMb This determines if the existing file (if present) is archived (see Archives) vs. opened in append mode. If the existing file is smaller than the specified number of megabytes (1 MB = 2^20 bytes), it is opened in append mode. Otherwise, it is archived. Therefore, if this property is 0 (the default), the file is always archived upon opening and new file is started.
MaxSizeMb This specifies how much the file is allowed to grow in megabytes (when the file is created instead of appended, it truly specifies the maximum size). What happens when the file has grown by this amount depends on the FullFilePolicy. It will wrap if circular logging is enabled (see CircularStartSizeKb and CircularStartDelaySeconds). If circular logging is not enabled when the file has grown by this amount, the text file is closed, archived (see Archives), and reopened. The binary file is simply closed.
FullFilePolicy This determines what happens when the file has grown to its maximum allowed size.

Wrap - If circular logging has started (based on CircularStartSizeKb and CircularStartDelaySeconds), the logger "wraps" back to the beginning of the circular part. If circular logging hasn't started, the behavior depends on whether it's a text file (the file is "rolled") or a binary file (the file is simply closed).

Roll - The file is closed, archived (see Archives), and reopened.

Close - The file is closed (Game Over).

Archives Specifies how many archives (backups) of the log file to keep. The value must be in the range 0 - 9999. If the existing file is not opened in append mode, it's archived by renaming it with '_01' appended to the file name. The existing _01 file is renamed _02, and so forth. Existing archives with numbers greater than the specified value are deleted. If the specified value is 0, the existing file is simply replaced when the new file is opened.
Use_00 If this bool is true, "_00" is appended to the file name specified by the Name property. This causes the file to be more logically sorted and grouped with its related "_01", "_02", etc., files in Explorer.
UseKbForSize If this bool is true, the properties MaxSizeMb and AppendIfSmallerThanMb have units of KiloBytes instead of MegaBytes.
CircularStartSizeKb This applies when FileFullPolicy is Wrap. When the file grows to this size in KB (1 KB = 1024 bytes), the portion of the file after this point (up the maximum size) becomes the circular part. The portion before this point is preserved regardless of how often the circular part wraps. Set this to 0 to disable this feature.
CircularStartDelaySeconds This applies when FileFullPolicy is Wrap. The circular log will start when the file has been opened this many seconds. Set this to 0 to disable this feature.

There are several additional read-only properties such as CurrentSize, CurrentPosition, and Wrapped.

File Events

Both file classes also have the following events you can attach handlers to. The "sender" object passed to each handler is the BinaryFile or TextFile object being opened or closed. If FullFilePolicy is Roll, these are raised every time the file is closed and reopened.

  • Opening - Raised just before the file is opened. This is the last chance to set properties like Directory, Name, MaxSizeMb, etc., before they are referenced by the internal code that does the actual opening, archiving, etc.
  • Opened - Raised after the file is opened.
  • Closing - Raised just before the file is closed.
  • Closed - Raised after the file is closed.

Binary File Logging

To send output to the binary file, do the following.

  1. Set the BinaryFileTraceLevel property of your Logger objects to send the required level of detail to the binary file. The default value is TraceLevel.Info, so you must change it in order to get any output from Logger.Debug() or Logger.Verbose(). Typically, you can just set Logger.Root.FileTraceLevel and all other loggers, if any, will inherit the value.
  2. Set the properties of Logger.DefaultBinaryFile, such as the Directory property, as required.
  3. Call Logger.DefaultBinaryFile.Open() to open the file. The file name, location, append mode, and other behaviors depend on the various properties of the object.

The class BinaryFile has these properties in addition to those given in the earlier table.

Property Description
Password The specified password will be used as an AES encryption key to encrypt the contents of the file and a one-way hash of the password will be stored in the file header. The Viewer application will prompt the user to enter the password. The hashes must match before the Viewer will open and decrypt the file. Performance is degraded by about 60% when using this feature.
PasswordHint If both Password and PasswordHint are not null or empty, the PasswordHint is stored in the file header and displayed to the user when the Viewer prompts for the password to open the file.
AddToListOfRecentlyCreatedFiles This boolean value determines whether or not the file will appear in the viewer's list of recently created files.

When the binary file is opened, some "standard data" about the current execution environment is automatically logged for you through a predefined Logger called StandardData. The purpose of the StandardData logger is to give you control of this output. For example, the Verbose output might be considered sensitive, so you can suppress it by coding the following before opening the file:

C#
Logger.StandardData.BinaryFileTraceLevel = TraceLevel.Debug; 
// or
Logger.StandardData.BinaryFileTraceLevel = TraceLevel.Off; 

Text File Logging

To send output to the text file, do the following.

  1. Set the TextFileTraceLevel property of your Logger objects to send the required level of detail to the text file. The default value is TraceLevel.Off, so you must change it in order to get any output in the text file. Typically, you can just set Logger.Root.TextFileTraceLevel and all other loggers, if any, will inherit the value.
  2. Set the properties of Logger.DefaultTextFile, such as the Directory property, as required.
  3. Call Logger.DefaultTextFile.Open() to open the file. The file name, location, append mode, and other behaviors depend on the various properties of the object.

In addition to the properties presented in the earlier table, the TextFile class has an additional property called FormatString that determines what fields are written to each record. It's similar to the format string you would pass to string.Format(), except it uses the following substitution parameters:

Substitution Parameter Type Description
{msg} string The log message passed to Logger.Info(), Logger.Debug(), etc.
{line} ulong Line number/counter. Initialized when the application starts (not when the file reopened), and incremented for each line written to the file.
{level} string One of the TracerX.TraceLevel enumeration values (e.g., Warning, Info, etc.)
{logger} string The name of the Logger object used to log the message.
{thnum} int The number assigned to the thread that logged the message. This number has nothing to do with Thread.ManagedThreadID or the OS thread ID. It's just a counter incremented by TracerX when a given thread calls it the first time.
{thname} string The name of the calling thread at the time the message was logged.
{time} DateTime The time in the system's local time zone when the message was logged.
{method} string The name of the method that logged the message. To get a meaningful value, you must use Logger.DebugCall(), Logger.InfoCall(), etc., in your code.
{ind} string Indentation (blanks). The length depends on the caller's stack depth. This changes when you call Logger.DebugCall(), Logger.InfoCall(), etc., in your code.

These substitution parameters work just like {0}, {1}, and so on that are passed to string.Format(). Format specifiers that are appropriate for the type of each substitution parameter can be included in the FormatString. For example, you might specify the following.

C#
Logger.DefaultTextFile.FormatString = '{line,6}, {time:HH:mm:ss.fff}, {msg}';

That will pad the line number to 6 spaces, include milliseconds with the timestamp, and separate the fields with commas.

Console Logging

To send "live" logging from your application to the console (i.e., command window), set the ConsoleTraceLevel property of your Logger objects to get the required level of detail (e.g., TraceLevel.Verbose for all log messages).

The format of this plain-text output is determined by Logger.ConsoleLogging.FormatString, which uses the same substitution parameters as Logger.DefaultTextFile.FormatString.

Debug Logging

This refers to output passed to System.Diagnostics.Trace.WriteLine(). This feature originally used Debug.WriteLine() (hence the term "debug logging"), but then I realized it might be useful in release builds. This output appears in the Output window of Visual Studio when your app is executed in the debugger. There are utilities that can intercept and display this output without Visual Studio.

To enable this output, set the DebugTraceLevel property of your Logger objects to get the required level of detail (e.g., TraceLevel.Verbose for all log messages).

The format of this plain-text output is determined by Logger.DebugLogging.FormatString, which uses the same substitution parameters as Logger.DefaultTextFile.FormatString.

Event Logging

Logging can be sent to the event log of your choice, but this should probably be used sparingly compared to the other destinations. For example, you should probably restrict it to certain loggers and/or trace levels.

Logger.EventLogging is a static class used to configure this feature. It has the following properties:

Property Description
EventLog 

Specifies the event log that TracerX uses for all event logging. The initial value is set to new EventLog("Application", ".", "TracerX - " + Assembly.GetEntryAssembly().GetName().Name). Refer to the MSDN documentation for the EventLog class. This property cannot be set in XML. It should be set programmatically, before parsing the XML or opening the log file, since those actions can generate internal events that also go to the event log specified by this property.

FormatString Specifies which fields are written to the event log. See the Text File Logging topic for substitution parameters.
MaxInternalEventNumber This applies to TracerX's internal events, not your log messages. It specifies the highest internal event number TracerX is allowed to log. More on this later. Default = 200.
InternalEventOffset A number added to TracerX's internal event numbers just before they are logged. Use this to prevent the event numbers logged by TracerX from conflicting with your application's event numbers. Default = 1000.
EventIdMap A Dictionary<TraceLevel, ushort> that specifies what event ID number to use for each TraceLevel value. This cannot be set in XML.
EventTypeMap A Dictionary<TraceLevel, EventLogEntryType> that specifies what EventLogEntryType to use for each TraceLevel value. This cannot be set in XML.

In addition to your log messages, TracerX can log internal events of its own, especially if it encounters errors opening the output file or parsing the XML configuration file. The internal event numbers fall into three ranges:

  • 1-100 = Error events, such as failing to open the log file due to insufficient permission.
  • 101-200 = Warning events, such as the log file reaching the maximum size without engaging circular logging.
  • 201-300 = Info events, such as the log file being opened successfully.

If you want to suppress all of these events, you can set Logger.EventLogging.MaxInternalEventNumber to 0. However, the recommended (and default) value is 200, so errors and warnings will be logged if they occur.

Event Handler Logging

The Logger class has a public static event called MessageCreated that can be raised whenever a message is logged. To enable this, set the EventHandlerTraceLevel property of your Logger objects to get the required level of detail (e.g., TraceLevel.Verbose for all log messages).

The "sender" parameter passed to the event handler is the Logger object through which the message was logged. The "args" parameter contains separate fields for the message's TraceLevel, thread name, logger name, message text, etc. This enables you to perform custom processing for any message. For example, you could do any of the following:

  • Send an email if the message came from a certain Logger.
  • Play a sound if the message's trace level is Error.
  • Write the message to a database table or file.
  • Set EventArgs.Cancel = true to prevent the message from reaching any other destination.

XML Configuration

Many of TracerX's configuration properties can be set via XML. The static class Logger.Xml contains the following methods for loading configuration settings from XML:

bool Configure(string configFile) Reads the TracerX element from the specified XML file.
bool Configure() Reads the TracerX element from your application's .config file. More on this later!
bool Configure(FileInfo configFile) Reads the TracerX element from the specified XML file.
bool Configure(Stream configStream) Reads the TracerX element from the specified Stream object.
bool ConfigureAndWatch(FileInfo configFile) Reads the TracerX element from the specified XML file, and uses a FileSystemWatcher to monitor the file for changes. If the file changes, it is parsed again.
bool ConfigureFromXml(XmlElement element) Extracts configuration settings from the specified XmlElement. The other methods end up calling this one.

If you use the XML configuration feature, you should call one of the above methods before opening Logger.DefaultBinaryFile or Logger.DefaultTextFile since many of the settings are for configuring those files. Instances of BinaryFile or TextFile that you create yourself aren't affected by the XML configuration.

TracerX will log an event if it encounters problems parsing the XML, such as an invalid element. Unless you want this event to go to the default event log (the Application event log) using the default source ("TracerX - <exe name>"), you should set the EventLogging.EventLog property before parsing the XML. Therefore, the recommended sequence of calls is...

C#
Logger.EventLogging.EventLog = new EventLog("Your Log", ".", "Your Source"); // Optional
Logger.Xml.Configure("YourTracerXConfig.xml");
Logger.DefaultBinayFile.Open();

The following XML sets every TracerX property that can be set via XML, except that additional loggers can also be created and configured. That is, you need not include every element shown.

XML
<?xml version="1.0" encoding="utf-8" ?>
<TracerX>

    <BinaryFile>
        <Directory value="%LOCALAPPDATA%" />
        <Use_00 value="false" />
        <AppendIfSmallerThanMb value="0" />

        <MaxSizeMb value="10" />
        <UseKbForSize value="false" />
        <FullFilePolicy value="Wrap" />

        <Archives value="3" />
        <CircularStartSizeKb value="300" />
        <CircularStartDelaySeconds value="60" />

        <AddToListOfRecentlyCreatedFiles value="true" />
    </BinaryFile>
    <TextFile>
        <Directory value="%LOCALAPPDATA%" />

        <Use_00 value="false" />
        <AppendIfSmallerThanMb value="0" />
        <MaxSizeMb value="2" />

        <UseKbForSize value="false" />
        <Archives value="3" />
        <FullFilePolicy value="Wrap" />

        <CircularStartSizeKb value="300" />
        <CircularStartDelaySeconds value="60" />
        <FormatString 
          value="{time:HH:mm:ss.fff} 
                 {level} {thname} {logger}+{method} {ind}{msg}" />

    </TextFile>
    <MaxInternalEventNumber value="200" />
    <InternalEventOffset value="1000" />
    <ConsoleFormatString
      value="{time:HH:mm:ss.fff} {level} {thname} {logger}+{method} {ind}{msg}" />

    <DebugFormatString
      value="{time:HH:mm:ss.fff} {level} {thname} {logger}+{method} {ind}{msg}" />
    <EventLogFormatString
      value="{time:HH:mm:ss.fff} {thname} {logger}+{method} {msg}"/>
    <Logger name="Root">
        <BinaryFileTraceLevel value="info" />

        <TextFileTraceLevel value="off" />
        <EventHandlerTraceLevel value="off" />
        <ConsoleTraceLevel value="off" />

        <EventlogTraceLevel value="off" />
        <DebugTraceLevel value="off" />
    </Logger>
    <Logger name="StandardData">

            <BinaryFileTraceLevel value="verbose" />
    </Logger>
</TracerX>

The TracerX element seen above can be in a file of its own, or it can be part of your application's .config file like this. Note the presence of the section tag that refers to the TracerX section.

XML
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="TracerX" type="TracerX.XmlConfigSectionHandler, TracerX" />

    </configSections>
    <TracerX>
        <LogFile>
                <Directory value="%EXEDIR%" />
        </LogFile>

    </TracerX>
</configuration>

I feel it's better to put the TracerX configuration in a separate XML file for the following reasons:

  1. A user could render the application unstartable by introducing syntax errors into the .config file when configuring TracerX.
  2. Some users do not have write access to the Program Files directory, which is where your app and its .config file will normally be.
  3. You may wish to send the user a new TracerX configuration file rather than ask him to edit it in order to enable a higher trace level, change the file size, or whatever. This is easier to do if it is separate from the app.config file.

Threads

The logger and viewer both support multi-threaded applications. The logger is thread-safe, and the viewer displays the thread name and thread number of each log message. The ability to filter by thread (e.g., view the output from a single thread) is especially useful.

Thread Names

Give your threads meaningful names whenever possible, including the main thread. This will help you understand the output in the viewer. A given thread can be named like this:

C#
Logger.Thread = "Some Name";

The above statement assigns the name "Some Name" to the current thread. Each thread can give itself a different name this way. A thread can change its name at any time.

If a thread's name hasn't been set via Logger.Thread, TracerX will attempt to use System.Threading.Thread.CurrentThread.Name. If that also hasn't been set, the viewer will generate names like "Thread 1" and "Thread 2".

Note that the thread object's System.Threading.Thread.CurrentThread.Name property is write-once, meaning an exception will be thrown if you try to assign it when it is not null. Therefore, you might want to test that Name is null before assigning it, or stick to using Logger.Thread which you can change at any time.

Thread IDs 

The thread number column seen in the viewer is not the thread's ManagedThreadId property. I found that if you repeatedly create and destroy threads, the CLR uses the same ID numbers over and over. That is, newly created threads tend to get the same ManagedThreadId as recently terminated threads. Since ManagedThreadId does not uniquely identify a thread for the life of a process, TracerX uses its own thread number, which is just a counter that is incremented every time it sees a new thread. The TracerX viewer allows you to filter by thread name or number.

Object Rendering

TracerX implements the log4net concept of custom object rendering using IObjectRenderer, DefaultRenderer, and RendererMap. These are the same interface/class names used by log4net. 

Most of the time, TracerX just calls an object's ToString() implementation to render the object as a string to be logged. When this behavior is not adequate for a given type, you can write an implementation of IObjectRenderer and add it to RendererMap to cause TracerX to call your code to render objects of the specified type (and derived types) as strings.

For example, suppose we want TracerX to render DateTimes using a certain format specifier that includes milliseconds, like "yyyy/MM/dd HH:mm:ss.fff". First, we write a class that implements IObjectRenderer. There is only one method to implement:

C#
using System;
using TracerX;

namespace Tester
{
    class DateTimeRenderer : IObjectRenderer
    {
        public void RenderObject(object obj, System.IO.TextWriter writer) 
        {
            DateTime dateTime = (DateTime)obj;
            writer.Write(dateTime.ToString("yyyy/MM/dd HH:mm:ss.fff"));
        }
    }
}

Somewhere in our code, we must add an instance of DateTimeRenderer to RendererMap, like this:

C#
RendererMap.Put(typeof(DateTime), new DateTimeRenderer());

Now, suppose we have a Logger named Log, and we use it to log the current time, with the following statement:

C#
Log.Info("The current time is ", DateTime.Now);

The DateTime passed to Log.Info is rendered with the registered renderer, and the result is:

The current time is 2007/11/23 14:56:06.765

Note that the following statement has the semantics of string.Format(), and therefore produces a different result:

C#
Log.InfoFormat("The current time is {0}", DateTime.Now);

The result (below) is different because the above statement passes the DateTime object directly to string.Format(), instead of rendering it with the registered renderer:

The current time is 11/23/2007 2:56:06 PM

The Default Renderer

If the object to render is not a string and its type (or base type) is not in RendererMap, TracerX attempts to use the pre-defined DefaultRenderer. This class has special handling for arrays, collections, and DictionaryEntry objects. If the object is not one of those, it simply calls ToString().

Exception Rendering

TracerX pre-loads RendererMap with a renderer for the Exception type. This renderer logs all nested inner exceptions, and reports more information than Exception.ToString(). You can remove this renderer from RendererMap by calling RendererMap.Clear().

TracerX vs. log4net

Similarities

  1. TracerX uses a hierarchy of loggers just like log4net.
  2. TracerX provides the same trace levels as log4net, plus one more (Verbose).
  3. The signatures of the log4net logging methods (e.g., Log.Info(), Log.Debug(), etc.) are duplicated in TracerX.
  4. TracerX can be configured via an XML file, the application config file, an XmlElement object, or programmatically.
  5. Output can be directed to multiple destinations.
  6. TracerX has the option of using a set of "rolling" text files.
  7. The RendererMap collection, the IObjectRenderer interface, and the DefaultRenderer object work the same in TracerX as in log4net.

Differences

  1. TracerX does not have a generic framework for adding multiple or custom appenders. It will not log user messages to a database table or email, although you can intercept them with the Logger.MessageCreated event and do whatever you want with them.
  2. TracerX does not support remote logging, but you can run TracerX-Service on a computer to enable remote viewing.
  3. TracerX does not have type converters, data formatters (other than IObjectRenderer), plug-ins, or repository hierarchies. Its implementation is relatively simple.

Advantages of TracerX

  1. TracerX has a powerful viewer.
  2. The viewer allows you to decide what threads, loggers, and trace levels you are interested in after the log is generated (as well as before).
  3. TracerX can perform circular logging within a single file. You can designate a portion of the initial logging (containing output from initialization/startup) to be preserved even after the log wraps. The viewer displays the output in chronological order.
  4. TracerX's binary file is more compact than log4net's text file.
  5. TracerX supports encrypted files.
This article was originally posted at https://github.com/MarkLTX/TracerX

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
United States United States
Mark Lauritsen has been a software developer since 1983, starting at IBM and using a variety of languages including PL/1, Pascal, REXX, Ada, C/C++ and C#. Mark currently works at a midstream energy company developing Windows services and desktop applications in C#.

Comments and Discussions

 
QuestionHave you updated this? Pin
arndibble3-Dec-22 4:23
arndibble3-Dec-22 4:23 
AnswerRe: Have you updated this? Pin
MarkLTX11-Apr-23 10:26
MarkLTX11-Apr-23 10:26 
SuggestionLog4Net .log and .txt file viewer Pin
Member 1167065416-May-20 4:48
Member 1167065416-May-20 4:48 
GeneralRe: Log4Net .log and .txt file viewer Pin
MarkLTX16-Jun-20 6:54
MarkLTX16-Jun-20 6:54 
GeneralRe: Log4Net .log and .txt file viewer Pin
Mitchell Crowsey1-Feb-21 5:52
Mitchell Crowsey1-Feb-21 5:52 
Question.NET Standard 2.0 support Pin
delfo11-Jan-18 22:08
delfo11-Jan-18 22:08 
AnswerRe: .NET Standard 2.0 support Pin
MarkLTX16-Jun-20 6:57
MarkLTX16-Jun-20 6:57 
QuestionUse github repository Pin
delfo14-Nov-17 8:49
delfo14-Nov-17 8:49 
AnswerRe: Use github repository Pin
Niels Peter Gibe28-Dec-17 10:25
Niels Peter Gibe28-Dec-17 10:25 
GeneralRe: Use github repository Pin
MarkLTX5-Jan-18 5:40
MarkLTX5-Jan-18 5:40 
Question64-bit error Pin
Eric Legault5-Jul-15 13:50
Eric Legault5-Jul-15 13:50 
AnswerRe: 64-bit error Pin
MarkLTX15-Sep-15 6:24
MarkLTX15-Sep-15 6:24 
QuestionCommon.Logging support Pin
delfo10-Mar-15 0:20
delfo10-Mar-15 0:20 
QuestionNice, but ETW is better Pin
fengyuancom2-Oct-14 2:56
fengyuancom2-Oct-14 2:56 
Quite powerful logger, but using ETW would be better.

For usage in multi-threaded process, locking could be a noticeable perf issue. ETW is much more efficient.
GeneralIt's really good! Pin
nandixxp27-Jul-14 3:19
nandixxp27-Jul-14 3:19 
QuestionIs it possible to get a TextWriter from TracerX Pin
delfo7-Jul-14 22:30
delfo7-Jul-14 22:30 
AnswerRe: Is it possible to get a TextWriter from TracerX Pin
MarkLTX8-Jul-14 16:56
MarkLTX8-Jul-14 16:56 
GeneralRe: Is it possible to get a TextWriter from TracerX Pin
delfo14-Jul-14 2:46
delfo14-Jul-14 2:46 
GeneralRe: Is it possible to get a TextWriter from TracerX Pin
MarkLTX14-Jul-14 15:35
MarkLTX14-Jul-14 15:35 
QuestionSmall mistake Pin
PCC7511-Nov-13 1:33
PCC7511-Nov-13 1:33 
QuestionAll that energy wasted to create YALF Pin
Darek Danielewski17-Jun-13 16:32
Darek Danielewski17-Jun-13 16:32 
AnswerRe: All that energy wasted to create YALF Pin
MarkLTX19-Jun-13 15:14
MarkLTX19-Jun-13 15:14 
GeneralRe: All that energy wasted to create YALF Pin
Darek Danielewski19-Jun-13 19:02
Darek Danielewski19-Jun-13 19:02 
SuggestionDefine an ILog interface Pin
delfo20-May-13 2:55
delfo20-May-13 2:55 
GeneralRe: Define an ILog interface Pin
MarkLTX23-May-13 14:24
MarkLTX23-May-13 14:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.