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:
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!
- You can filter and/or colorize the log by thread name, thread number, logger, trace level, text wildcards, and method name.
- The message text is indented according to stack depth.
- You can collapse and expand method calls by double-clicking rows where "+" and "-" appear.
- You can navigate up and down the call stack using the crumb bar and/or context menu.
- You can click the arrows in the crumb bar to see (and jump to) the methods called at a given level.
- You can view absolute or relative timestamps. Any row can be the "zero time" record.
- You can collapse and expand rows that contain embedded newlines by double-clicking lines with yellow triangles.
- You can bookmark individual rows, or all rows that...
- contain a specified search string.
- are from one or more selected threads, loggers, or trace levels.
- You can view the call stack (in a window) leading to a selected row, and navigate to rows in the call stack.
- You can jump to the next block from the same thread, or the next block from a different thread.
- You can select rows and copy the text column or all columns to the clipboard.
- You can customize the columns (show/hide columns, change their order).
- 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:
using TracerX;
namespace HelloWorld
{
class Program
{
static Logger Log = Logger.GetLogger("Program");
static void Main(string[] args)
{
Logger.DefaultBinaryFile.Open();
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).
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 Logger
s to create and what to name them. One approach is to create a Logger
for each class in your app, like this:
using TracerX;
class CoolClass
{
private static readonly Logger Log =
Logger.GetLogger("CoolClass");
}
The static method Logger.GetLogger()
is the only way to create Logger
s in your code. It has these overloads:
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 Logger
s. 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 Logger
s 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:
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:
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
):
private void YourMethod()
{
using (Log.InfoCall())
{
}
}
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:
- TracerX obtains the name of the calling method (i.e., "YourMethod").
- A message like "YourMethod: entered" is logged.
- Indentation is increased for any logging done by the calling thread inside the "
using
" block. - The viewer displays "YourMethod" in the Method column for any logging inside the "
using
" block. - 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:
- You plan to obfuscate your code (and you don't want the obfuscated method names in the log).
- You want to simulate a method call in an "
if
" statement, "for
" loop, or other block. - 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.
using System;
using System.Threading;
using System.IO;
using TracerX;
namespace Sample
{
class Program
{
private static readonly Logger Log = Logger.GetLogger("Program");
private static bool LogFileOpened = InitLogging();
private static bool InitLogging()
{
Thread.CurrentThread.Name = "MainThread";
Logger.Xml.Configure("TracerX.xml");
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:
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 Logger
s 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 Logger
s 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 Logger
s 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 TraceLevel
s. A typical scenario would be for an application to set the following levels:
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 Logger
s 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 Logger
s (or groups of Logger
s).
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:
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:
Logger.DefaultBinaryFile.Directory = "C:\\Logs";
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.
- 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. - Set the properties of
Logger.DefaultBinaryFile
, such as the Directory
property, as required. - 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:
Logger.StandardData.BinaryFileTraceLevel = TraceLevel.Debug;
Logger.StandardData.BinaryFileTraceLevel = TraceLevel.Off;
Text File Logging
To send output to the text file, do the following.
- 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. - Set the properties of
Logger.DefaultTextFile
, such as the Directory
property, as required. - 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.
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...
Logger.EventLogging.EventLog = new EventLog("Your Log", ".", "Your Source");
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.
="1.0"="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.
="1.0"="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:
- A user could render the application unstartable by introducing syntax errors into the .config file when configuring TracerX.
- Some users do not have write access to the Program Files directory, which is where your app and its .config file will normally be.
- 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:
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 string
s.
For example, suppose we want TracerX to render DateTime
s 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:
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:
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:
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:
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
- TracerX uses a hierarchy of loggers just like log4net.
- TracerX provides the same trace levels as log4net, plus one more (
Verbose
). - The signatures of the log4net logging methods (e.g.,
Log.Info()
, Log.Debug()
, etc.) are duplicated in TracerX. - TracerX can be configured via an XML file, the application config file, an
XmlElement
object, or programmatically. - Output can be directed to multiple destinations.
- TracerX has the option of using a set of "rolling" text files.
- The
RendererMap
collection, the IObjectRenderer
interface, and the DefaultRenderer
object work the same in TracerX as in log4net.
Differences
- 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. - TracerX does not support remote logging, but you can run TracerX-Service on a computer to enable remote viewing.
- TracerX does not have type converters, data formatters (other than
IObjectRenderer
), plug-ins, or repository hierarchies. Its implementation is relatively simple.
Advantages of TracerX
- TracerX has a powerful viewer.
- 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).
- 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.
- TracerX's binary file is more compact than log4net's text file.
- TracerX supports encrypted files.
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#.