Introduction
Sometimes when you are developing or testing applications, you can find code running into exceptional conditions. This manifests itself in a number of ways - hangs, crashes, slowdowns. Sometimes, this happens as a result of an earlier failure, or sometimes, things fail during a lengthy process.
I found that often there would be something in the log file that explained an issue, and the earlier I was aware of this, the faster I was able to troubleshoot something in an unfamiliar area of an application.
This utility monitors a whole directory (or more) of log files and instantly alerts when an error occurs - this has proven to be very useful for certain situations.
Background
In legacy products, there are often log files, often all different formats.
This popup viewer attempts to be a lowest common denominator for scanning log files - it treats each line as a new entry, expects a plain text format with carriage return separators, and alerts to the system tray whenever a line with the word "error" somewhere in it is appended to the log file.
Using the Code
The code is compiled using Visual Studio 2012 using .NET 4, however it should be easily opened and compiled in any older version as there is very little in the way of advanced code or features or references - it is a small self contained utility.
I made it multi-threaded and multi-instance to solve some common problems - the threading is to keep the UI responsive while looking at the log files in the main form, and the multi-instance is set up to allow you to open several instances with different files to read on the screen with only one of the instances scanning the actual log files for popping alerts.
I simplified the configuration by putting all of the stored settings into the registry and using them directly via properties - this means that if you have more than one instance open at once, the settings propagate across immediately and any changes to settings are preserved.
public bool PopupErrors
{
get { return (_registryHelper.readKey
("PopupErrors", "true").ToLower() == "true") ? true : false; }
set { _registryHelper.writeKey("PopupErrors", value.ToString()); }
}
I scan through directories individually rather than recursively as any permission errors on subdirectories were throwing it out otherwise and ignoring the whole tree.
private string[] FindLogFilesInDirectory(string dir)
{
string[] logfiles = Directory.GetFiles(dir, "*.log", SearchOption.TopDirectoryOnly);
string[] subdirs = Directory.GetDirectories(dir);
foreach (string subdir in subdirs)
{
try
{
string[] moreFiles =
Directory.GetFiles(subdir, "*.log", SearchOption.TopDirectoryOnly);
logfiles = logfiles.Concat(moreFiles).ToArray();
}
catch (UnauthorizedAccessException) { }
}
return logfiles;
}
I tail each file on each scan and after scanning, I store the file position - so next pass I only scan the newly added portion on each file. Files that shrink are assumed to be started over by a log writer so I scan from the beginning. Files that suddenly appear out of nowhere as a big file are assumed to be a log writer archiving the file and starting a new one so the large files are ignored.
private void TailFile(string filename)
{
try
{
using (StreamReader reader = new StreamReader(new FileStream
(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
long lastRead = GetLastReadPosition(filename);
if (reader.BaseStream.Length != lastRead)
{
if (!(lastRead == 0 && reader.BaseStream.Length -
lastRead > 1000000))
{
if (lastRead > reader.BaseStream.Length)
lastRead = 0;
reader.BaseStream.Seek(lastRead, SeekOrigin.Begin);
string line = "";
while ((line = reader.ReadLine()) != null)
{
Invoke(ShowNotifyWindowIfRequiredEvent, line, filename);
}
SetLastReadPosition(filename, reader.BaseStream.Position);
}
else
{
SetLastReadPosition(filename, reader.BaseStream.Length);
}
}
}
}
catch (IOException) { }
}
private void SetLastReadPosition(string filename, long pos)
{
if (_lastReadPosition.ContainsKey(filename))
_lastReadPosition[filename] = pos;
else
_lastReadPosition.Add(filename, pos);
}
private long GetLastReadPosition(string filename)
{
if (_lastReadPosition.ContainsKey(filename))
return _lastReadPosition[filename];
return 0;
}
Points of Interest
The error message will pop up as a small window to the lower right of the screen. Hovering over this will stop it automatically hiding after 3 seconds. Clicking on it will open the associated log file at the message location.
When viewing log files, the arrow keys will scroll automatically to next or previous error messages. Page up and down scroll to next or previous 'warning' messages. Ctrl-F or F3 work for finding particular text.
All of the log files scanned can be individually viewed by choosing them from the drop down.
To specify a directory to scan, either put it in the .exe.config file or right click on the application tray icon and choose to enter an additional scan directory. If you give it e.g. "C:\Windows", you will get too much information to be useful - so best to limit it to log files you are interested in. Separate directories with '|
'
="1.0"="utf-8"
<configuration>
<appSettings>
<add key="LogDirectories" value="%LocalAppData%|%userprofile%"/>
</appSettings>
</configuration>
Most of it was cobbled together from other CodeProject articles - too many to reference individually, but thanks to others for their parts of this.
History
- 2nd July, 2019: First version published