Introduction
I often need to watch text-based log files for services or IIS logs. I got tired of having to refresh the file in my editor or having to close and re-open the file to see the latest changes. So, I wrote a tool that would do the work for me and show me the latest additions to a log file continuously. The application is pretty full-featured. It supports the following:
- Multiple log files can be open at the same time.
- File names can be passed as arguments to the application to be loaded at startup.
- A TabControl is used to switch between the log files being monitored.
- Files can be dropped onto the application.
- Logs can be updated via a timer or immediately using FileSystemWatcher.
- Updates can be paused.
- Only the lines added to the log since the last update are read in to optimize performance.
- If the file is too large then only the end of the log is read in.
- Correctly handles log files that only terminate lines with linefeed.
- Settings and window position are saved in the configuration file.
Using the code
To work with the source code, just unzip the Source Files and open the LogMonitor.sln solution file. This was written using .NET 2.0 entirely in C#.
Points of interest
LogMonitorControl user control
The main worker class in this application is LogMonitorControl
, which is a user control that watches a single log file. A TabControl
is used to hold multiple instances of LogMonitorControl
.
Threading concerns
One tricky bit was that the LogMonitorControl
can use a Timer
or a FileSystemWatcher
to monitor the log file. These behave differently in terms of threading. When using the Timer
, the timer tick callbacks occur on the main thread. However, when using the FileSystemWatcher
method, the callback happens on a different thread. So, updating the child controls involved checking whether or not Invoke should be used via the InvokeRequired
property. Anonymous delegates help greatly with this. However, if a lot of code is to be executed for a control, then it is also convenient to write a method that performs those actions. Then either Invoke that method or call it directly. Below is an example of where this is needed. Note that the method DoScrollToEnd
is a method of the LogMonitorControl
class and not a member of the control Invoking the method:
public void ScrollToEnd()
{
if (_textBoxContents.InvokeRequired)
{
_textBoxContents.Invoke(new MethodInvoker(DoScrollToEnd));
}
else
{
DoScrollToEnd();
}
}
Drag & drop support
(Well, just drop support really.)
Adding drop support was very easy and consisted of only 2 methods. The code is as follows:
private void tabControl_DragDrop(object sender, DragEventArgs e)
{
IDataObject data = e.Data;
if (data.GetDataPresent(DataFormats.FileDrop))
{
string[] filesToDrop =
(string[])e.Data.GetData(DataFormats.FileDrop, false);
for (int idx = 0; idx < filesToDrop.Length; idx++)
{
MonitorNewFile(filesToDrop[idx]);
}
}
}
private void tabControl_DragEnter(object sender, DragEventArgs e)
{
IDataObject data = e.Data;
if (data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Copy;
}
else
{
e.Effect = DragDropEffects.None;
}
}
FileSystemWatcher
The FileSystemWatcher
class is found in the System.IO
namespace and is used to monitor changes to a file or any file in a directory. The constructor takes a path and a filename pattern. The LogMonitor application calls the constructor with a filename for the pattern to monitor a single file. We are interested in all of the change events that this class can raise, so we sign up for all of them. We could actually track this file through renames and continue to monitor the log file, but I choose to simply keep watching the same file. Then if the file is renamed back, we will detect that. Similarly, if another file is created with the same filename, that will also be detected. There is one thing that is not completely obvious about the FileSystemWatcher
class. That is, if you sign up for the file events you will never receive them unless you set the property EnableRaisingEvents
to true.
_watcher = new System.IO.FileSystemWatcher(path, baseName);
FileSystemEventHandler handler = new FileSystemEventHandler(watcher_Changed);
_watcher.Changed += handler;
_watcher.Created += handler;
_watcher.Deleted += handler;
_watcher.Renamed += watcher_Renamed;
_watcher.EnableRaisingEvents = true;
Open file with lowest amount of locking
Some applications keep the log file open during execution, like IIS. To give the LogMonitor the best chance of working with these types of applications, it opens the log file with the friendliest settings possible. The easiest way to read a text file in .NET is to use the File.ReadAllText()
method. However, this does not open the file with the proper sharing flags. So, I open the file using File.Open
and specify the access and sharing modes. Then open a StreamReader
and use ReadToEnd
on that stream. The code for reading the file is shown below, with some lines that are irrelevant to this point having been removed:
string newFileLines = "";
using (FileStream stream = File.Open(_fileName,
FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (StreamReader reader = new StreamReader(stream))
{
newFileLines = reader.ReadToEnd();
}
}
History
- 5 June, 2007 -- Original version posted
- 6 June, 2007 -- Version 1.0.0.1
- Fixed bug with trimming text for large files. (Thanks for the catch s_mom).