Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / file

Popup Log Viewer

5.00/5 (6 votes)
1 Jul 2019CPOL3 min read 14.1K   378  
A tray utility for monitoring log files and popping up error messages

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.

C#
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.

C#
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) { } // ignore directories 
                                              // we're prohibited from seeing
   }
   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.

C#
private void TailFile(string filename)
{
    try
    {
        using (StreamReader reader = new StreamReader(new FileStream
        (filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
        {
            // only read more if the file size has changed
            long lastRead = GetLastReadPosition(filename);
            if (reader.BaseStream.Length != lastRead)
            {
                if (!(lastRead == 0 && reader.BaseStream.Length - 
                	lastRead > 1000000))  // ignore new files more than 1MB that 
                			// suddenly appear, just a log file wrapping and 
                			// we don't want to see the old log flash past our eyes
                {
                    if (lastRead > reader.BaseStream.Length)
                        lastRead = 0;  // if the log file got smaller it's because 
                        // it's wrapped and started over, so start again from the beginning
                    //seek to the last max offset
                    reader.BaseStream.Seek(lastRead, SeekOrigin.Begin);
                    //read out of the file until the EOF
                    string line = "";
                    while ((line = reader.ReadLine()) != null)
                    {
                        Invoke(ShowNotifyWindowIfRequiredEvent, line, filename);
                    }
                    //update the last read position
                    SetLastReadPosition(filename, reader.BaseStream.Position);
                }
                else
                {
                    //update the last read position to file length
                    SetLastReadPosition(filename, reader.BaseStream.Length);
                }
            }
        }
    }
    catch (IOException) { } // occasionally files are being written to, ignore these
}
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 '|'

XML
<?xml version="1.0" encoding="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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)