A small amount of code can add powerful and tailored debug facilities. category 'KB', language C#, created 03-Jun-2010, version V1.1 (07-Jun-2016), by Luc Pattyn |
License: The author hereby grants you a worldwide, non-exclusive license to use and redistribute the files and the source code in the article in any way you see fit, provided you keep the copyright notice in place; when code modifications are applied, the notice must reflect that. The author retains copyright to the article, you may not republish or otherwise make available the article, in whole or in part, without the prior written consent of the author. Disclaimer: This work is provided “as is”, without any express or implied warranties or conditions or guarantees. You, the user, assume all risk in its use. In no event will the author be liable to you on any legal theory for any special, incidental, consequential, punitive or exemplary damages arising out of this license or the use of the work or otherwise. |
This article describes "technical logging", i.e. logging intended as an aid in the development process, while code is being created and debugged by programmers. As such it would not be part of the product's requirements, as it is not concerned with "application-level logging" where mostly statistical information would be gathered about the application domain, nor "audit trailing" that may be required by some regulatory autorities.
In my view logging mainly means collecting single-line messages while a program runs, capturing what the program is doing, together with relevant data values, and whatever can be useful to verify the program is behaving as expected, but also to investigate things that go wrong occasionally.
Modern debuggers are powerful tools; they support break points, single-stepping, watching intermediate values, etc., all at the spur of the moment. That turns out to be useful for solving problems, unfortunately it works best for rather simple problems, and less so for more complex situations, such as:
Logging should be
There are a number of highly regarded log packages. I choose not to use them, as they are more expensive: they typically require more initialization, an additional DLL file, and more computer resources as they tend to implement a Swiss-knife approach, offering every feature anybody could dream of.
Writing your own will result in something lean-and-mean, that can easily be tailored to specific needs without the Swiss expense.
A very simple example is given here:
class myClass {
private static void log(string s) {
Console.WriteLine(s);
}
public myClass() {
log("myClass constructor begin");
...
for(int step=0; step<4; step++) {
log("step = "+step);
...
}
log("myClass constructor end");
}
}
I tend to keep the log method static, as it should not rely on instance data, and having it available to static methods is a plus too.
All classes should use the same log method; it makes perfect sense to have a separate static class holding the actual log method. On the other hand, we want to keep the log statement as simple as possible, so the recommended approach is this:
class LogClass {
public static void Log(string s) {
Console.WriteLine(s);
}
}
class AnotherClass {
protected static log(string s) {
LogClass.Log("AnotherClass "+s);
}
public AnotherClass() {
log("constructor begin");
...
log("constructor end");
}
}
Having a local stub keeps the log statements simple; it also allows for an easy insertion of the
class name in the log message; and it can be used to turn logging on or off for each class individually,
e.g. by commenting the LogClass.Log()
statement.
I consider it essential to include a time stamp. When using multiple threads, I also want to identify the active thread. It often results in this:
class LogClass {
public static void Log(string s) {
if (s.Length!=0) {
int TID=Thread.GetCurrentThread().ManagedThreadId;
s=DateTime.Now.ToString("HH:mm:ss.ff")+" ["+TID.ToString("X2")+"] "+s;
}
Console.WriteLine(s);
}
}
If time stamps need millisecond accuracy, I combine DateTime.Now with a free-running Stopwatch to get the milliseconds right, as DateTime.Now itself only ticks once every 10 to 30 milliseconds (see my timers article for more on this).
So far everything got logged towards a Console, which is either the Command Prompt window when running a Console application, or an explicitly created Console window when running a WinForms application.
It is often extremely useful to log to a file. I often want the file to be closed most of the time, which means each write operation basically consists of opening the file, appending one line, and closing it again. The reason I want this is because, when the application crashes, the last, i.e. most recent log lines are probably the most relevant ones. Hence, no buffering, and no open files. In other situations, when a lot of information is required, and a crash isn't going to happen, a scheme with a permanently open file may be preferred.
Writing one line of text to a closed file is extremely easy; just add one line to the Log() method:
File.AppendAllText(logPath, s+Environment.NewLine);
where logPath
is a static string in the LogClass, holding the path of the log file.
Suggestions:
Example:
class LogClass {
private static string CompanyName="myCompany";
private static string AppName="myApplication";
private static string logPath;
public static void Log(string s) {
if (s.Length!=0) {
int TID=Thread.GetCurrentThread().ManagedThreadId;
s=DateTime.Now.ToString("HH:mm:ss.ff")+" ["+TID.TiString("X2")+"] "+s;
}
Console.WriteLine(s);
if (logPath==null) CreateLogFile();
File.AppendAllText(logPath, s+Environment.NewLine);
AppendToListBox(s); // see next chapter
}
private static void CreateLogFile() {
string folder=Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)+@"\"+
CompanyName+@"\"+AppName+@"\";
Directory.CreateDirectory(folder);
string[] oldLogs=File.GetFiles(folder, "log*.txt");
foreach(string f in oldLogs) { // check age and try-delete if old }
DateTime now=DateTime.Now;
logPath=folder+"log_"+now.ToString("yyyyMMdd")+".txt";
File.WriteAllText(logPath, "Started at "+now.ToString());
}
}
The ideal Control for accepting log messages is the System.Windows.Forms.ListBox
as it
is line-oriented and does not suffer from degrading performance when thousands of messages get added.
As soon as multiple threads (or asynchronous handlers) are involved, one must take measures to get the ListBox accesses limited to the GUI thread (see this cross-thread article), as the code below does:
private void AppendToListBox(string s) {
if (myListBox.InvokeRequired) {
myListBox.Invoke(new Action<string>(AppendToListBox), new object[] {s} );
} else {
myListBox.Items.Add(s);
if (Control.MouseButtons==0) myListBox.TopIndex=myListBox.Items.Count-1;
}
}
The TopIndex
statement implements a little feature where the ListBox scrolls up,
unless you press a mouse button (maybe because you want to scroll manually in the very same ListBox).
As scrolling eats CPU cycles, you may consider skipping it all together.
Time-related warning: I use Invoke rather than BeginInvoke, as this best preserves the chronology of the log messages; but even that will not be perfect, it is possible some messages get stalled in the input queue of the GUI thread. Also note that, in all code shown, the time stamp is added early on, so it, rather than the relative order of the log messages should be taken into account.
Sometimes one does not want to obtain the output of all available log statements, and some build-time or even run-time control over the logging is wanted. Build-time control could be as easy as commenting/ uncommenting a line in the local log() method of the relevant classes.
Run-time control could be quite elaborate. My largest applications offer a control panel, where different categories can be enabled and disabled while the application is running.
One rather easy scheme for run-time control involves assigning each log statement to a particular category, and passing the category as a parameter or use separate methods. Here is an example:
class AnotherClass {
protected static error(string s) {
if (logErrror) LogClass.Log("error: AnotherClass "+s);
}
protected static info(string s) {
if (logInfo) LogClass.Log("info: AnotherClass "+s);
}
public AnotherClass() {
info("constructor begin");
...
info("constructor end");
}
}
Whatever scheme you implement, I strongly advise against ever removing a log statement: if you don't want it, make it disappear from the program flow (using a comment, or an if statement), but don't throw away the result of earlier efforts, instead keep it around until you want it again.
When logging statements become more complex and a lot of them will not execute because of log conditions, a lot of string manipulations tend to become useless. If so, one could consider implementing a number of log methods, each taking a different number of parameters, that only get merged into the final log message once it has been established the log will pass the log conditions.
Example:
class AnotherClass {
protected static log(string s, int arg0, int arg1, int arg2) {
if (logCondition) {
s=string.Format(s, arg0, arg1, arg2);
LogClass.Log("AnotherClass "+s);
}
}
public AnotherClass() {
log("constructor minimum={0} maximum={1} value={2}", min, max, val);
...
}
}
The Art of Logging, an excellent article by Colin Eberhardt, is available on CodeProject.
There is no limit to what one can include in a logging mechanism. The advantage of doing it yourself is, it only takes a small amount of code and can easily be tailored for a good balance between functionality and performance.
Perceler |
Copyright © 2012, Luc Pattyn |
Last Modified 02-Sep-2013 |