ILoggable

A place to keep my thoughts on programming

 Subscribe

geekblog
[at]
claassen [dot] net

Powered by Blogger

Wednesday, June 01, 2005

Log4Net ScrollingTextBox

I use log4net for most of my apps. I did similar things in all the languages i've worked with. I just like having a way to verbosely spew information into the ether so that i could use for analysing code when something gest wonky without having to step through the code or go uncommenting loads of Console.Writeline() calls.

On unix, i just always have a terminal open for tailing the log files and while i often do the same under Windows, using a Cygwin bash shell, i've started putting debug windows into my Windows.Forms apps, just because it's nice and convenient. To make this work, you simply use the log4net MemoryAppender. For display purposed, I use a TextBox, but do some legwork to limit the size of the text logged and to make sure it stays scrolled at the bottom.

public class FormX : System.Windows.Forms.Form
{
    #region Static Members ####################################################
    private static readonly ILog log = LogManager.GetLogger(typeof(FormX));
    #endregion

    #region Members Variables #################################################
    private bool logWatching = true;
    private log4net.Appender.MemoryAppender logger;
    private Thread logWatcher;
    /// <summary>
    /// The TextBox for our logging messages
    /// </summary>
    private System.Windows.Forms.TextBox mLog;
    /// <summary>
    /// Required designer variable.
    /// </summary>
    private System.ComponentModel.Container components = null;
    #endregion

    #region Constructors #################################################
    public FormX()
    {
        //
        // Required for Windows Form Designer support
        //
        InitializeComponent();

        this.Closing += new CancelEventHandler(FormX_Closing);
        logger = new log4net.Appender.MemoryAppender();

        // Could use a fancier Configurator if you don't want to catch every message
        log4net.Config.BasicConfigurator.Configure(logger);

        // Since there are no events to catch on logging, we dedicate
        // a thread to watching for logging events
        logWatcher = new Thread(new ThreadStart(LogWatcher));
        logWatcher.Start();
    }
    #endregion

    // [...]

    private void FormX_Closing(object sender, CancelEventArgs e)
    {
        // Gotta stop our logging thread
        logWatching = false;
        logWatcher.Join();
    }

    private void LogWatcher()
    {
        // we loop until the Form is closed
        while(logWatching)
        {
            LoggingEvent[] events = logger.Events;
            if( events != null && events.Length > 0 )
            {
                // if there are events, we clear them from the logger,
                // since we're done with them
                logger.Clear();
                foreach( LoggingEvent ev in events )
                {
                    StringBuilder builder;
                    // the line we want to log
                    string line = ev.LoggerName + ": " + ev.RenderedMessage+"\r\n";
                    // don't want to grow this log indefinetly, so limit to 100 lines
                    if( mLog.Lines.Length > 99 )
                    {
                        builder = new StringBuilder(mLog.Text);
                        // strip out a nice chunk from the beginning
                        builder.Remove(0,mLog.Text.IndexOf('\r',3000)+2);
                        builder.Append(line);
                        mLog.Clear();
                        // using AppendText since that makes sure the TextBox stays
                        // scrolled at the bottom
                        mLog.AppendText(builder.ToString());
                    }
                    else
                    {
                        mLog.AppendText(line);
                    }
                }
            }
            // nap for a while, don't need the events on the millisecond. 
            Thread.Sleep(500);
        }
    }
}

4 Comments:

At 9:23 AM, Blogger mtv said...

Hi
Very nice code...
But there are two big problems in your code...

1. You are changing content of the control mLog. It is not obvious from your code, but I suggest that you are creting this control in the main thread. It is not thread safe to create control in one thread and to change it from another.

2. MemoryAppender is also not thread safe. So if I log some info from another thread like 'LogWatcher' it is not sure, that it will be corectly displayd. (my log action from another thread will occure between logger.Events and logger.Clear())

Do you know some sollution for the second problem? (the first one is possible to solve through Invoke method)

Thanks
mtv

 
At 7:21 AM, Blogger ether said...

mtv,

I believe problem 1) is addressed with my revised version here. This code uses BeginInvoke to make sure that the update happens on the UI thread.

Problem 2) would explain why i still get the occasional lock-ups. That could also be solved by using begininvoke and either handling the whole memoryappender issue on the UI thread or creating a separate thread for the appender and invoking actions on that thread via BeginInvoke. The first option is easier to write, but may impact the UI responsiveness.

HTH,
ether

 
At 7:48 AM, Blogger ether said...

mtv,

regarding problem 2).. What I previously wrote won't do the trick. What needs to happen is that your log.Debug() calls happen on one thread. So you basically need a wrapper for it that invokes all logging on a single thread.

 
At 5:08 AM, Blogger eugene said...

Hi,
Thank you very match. I've used your code in my project, but I want to say, that add text to control is not thread safe. I've added some codes for making calls thread-safe (How to: Make Thread-Safe Calls to Windows Forms Controls). I've added msgs to ListBox therefore I can add code only for ListBox
delegate void AddMsgCallback(string msg);

private void AddMsg(string msg)
{
if (this.lbLog.InvokeRequired)
{
AddMsgCallback d = new AddMsgCallback(AddMsg);
this.Invoke(d, new object[] { msg });
}
else
{
if (this.lbLog.Items.Count > 1000)
{
this.lbLog.Items.RemoveAt(0);
}
this.lbLog.Items.Add(msg);
}
}

 

Post a Comment

<< Home