Website Log Tail

One of the downsides of our shared hosting plan is that we don’t have any access to the box, except for FTP. Marblespark.com uses log4net extensively and the logs are used to track all sorts of things. Since I don’t have any type of RDP or other terminal access to the server, I needed something to view the logs.

First Attempt

The first attempt was a very, very basic asp.net page with a drop down for the log files, a text box for number of lines to read, a text area to display the lines and one button “View Log”. Seemed very simple, until I needed to read a log file that was already opened for writing.

fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

Next, I needed a way to read a file backward. Gee, how hard can that be… Luckily, I was able to find the code (forgot where) and here’s the guts of it. Search on c# backward reader and you’ll find it.

   1: public string Readline()

   2: {

   3:     byte[] line;

   4:     byte[] text = new byte[1];

   5:     long position = 0;

   6:     int count;

   7:     fs.Seek(0, SeekOrigin.Current);

   8:     position = fs.Position;

   9:

  10:     //do we have trailing \r\n?

  11:     if (fs.Length > 1)

  12:     {

  13:         byte[] vagnretur = new byte[2];

  14:         fs.Seek(-2, SeekOrigin.Current);

  15:         fs.Read(vagnretur, 0, 2);

  16:         if (ASCIIEncoding.ASCII.GetString(vagnretur).Equals("\r\n"))

  17:         {

  18:             //move it back

  19:             fs.Seek(-2, SeekOrigin.Current);

  20:             position = fs.Position;

  21:         }

  22:     }

  23:

  24:     while (fs.Position > 0)

  25:     {

  26:         text.Initialize();

  27:         //read one char

  28:         fs.Read(text, 0, 1);

  29:         string asciiText = ASCIIEncoding.ASCII.GetString(text);

  30:         //moveback to the charachter before

  31:         fs.Seek(-2, SeekOrigin.Current);

  32:         if (asciiText.Equals("\n"))

  33:         {

  34:             fs.Read(text, 0, 1);

  35:             asciiText = ASCIIEncoding.ASCII.GetString(text);

  36:             if (asciiText.Equals("\r"))

  37:             {

  38:                 fs.Seek(1, SeekOrigin.Current);

  39:                 break;

  40:             }

  41:         }

  42:     }

  43:     count = int.Parse((position - fs.Position).ToString());

  44:     line = new byte[count];

  45:     fs.Read(line, 0, count);

  46:     fs.Seek(-count, SeekOrigin.Current);

  47:     return ASCIIEncoding.ASCII.GetString(line);

  48: }

So, to read an open log file and dump it to the text area:

   1: protected void Button1_Click(object sender, EventArgs e)

   2: {

   3:     tbData.Text = "";

   4:     int lineCnt = 1;

   5:     List<string> lines = new List<string>();

   6:     int maxLines = int.Parse(tbLines.Text);

   7:     string logFile = Server.MapPath("~/" + ddlLogFiles.SelectedItem.ToString());

   8:     BackwardReader br = new BackwardReader(logFile);

   9:     tbData.Text = "";     while (!br.SOF)

  10:     {

  11:         string line = br.Readline();

  12:         lines.Add(line + System.Environment.NewLine);

  13:         if (lineCnt == maxLines) break;

  14:         lineCnt++;

  15:     }

  16:

  17:     for (int i = lines.Count; i > 0; i--)

  18:     {

  19:         this.tbData.Text += lines[i - 1];

  20:     }

  21: }

Initially, this worked well for a while, but I got tired of clicking “View Log” over and over and realized that I wanted a web based Tail program, like Baretail, which I use on my laptop.

Second Attempt

My next attempt was to simply use javascript’s setTimeout to refresh the page. While this worked, it seemed clunky, so I decided to ajax it. But, being a lazy programmer, I didn’t everything that WCF offers; I needed something simple and quick. I remembered reading a couple of articles about adding a static, “Webmethod” to a page. So, that’s what I did. In a previous post, I mentioned using Rick Strahl’s serviceproxy, which makes the process as easy as can be.

So the details for implementing my web based tail applications goes like this:

  • Create the ajax method as part of the web page.
  • Add serviceproxy.js, json2.js and js_extensions.js to enable ajax & displaying the logs
  • Replace the text area with a standard div; more on this below…
  • Finally some handy, dandy css to make it look nicer.

Step 1 – The Webmethod

   1: [System.Web.Services.WebMethod]

   2: public static IList<string> GetLogTail(string logname, string numrows)

   3: {

   4:     int lineCnt = 1;

   5:     List<string> lines = new List<string>();

   6:     int maxLines;

   7:

   8:     if (!int.TryParse(numrows, out maxLines))

   9:     {

  10:         maxLines = 100;

  11:     }

  12:

  13:     string logFile = HttpContext.Current.Server.MapPath("~/" + logname);

  14:

  15:     BackwardReader br = new BackwardReader(logFile);

  16:     while (!br.SOF)

  17:     {

  18:         string line = br.Readline();

  19:         lines.Add(line + System.Environment.NewLine);

  20:         if (lineCnt == maxLines) break;

  21:         lineCnt++;

  22:     }

  23:     lines.Reverse();

  24:     return lines;

  25: }

It worked much better once I added “lines.Reverse()” before returning the data. So the data returned is standard json, except there’s a “d” object that’s prepended to the data. Rick’s serviceproxy handles this, so no worries.

Step 2 – JavaScript

serviceproxy.js and json2.js are used to deal with the asp.net peculiarities of ajax of asp.net. See the links above; Rick’s site covers these in great detail.

js_extensions.js is my growing collection of javascript extension methods. Currently, this is pretty small and contains other peoples work for formatting dates and HTML encoding and decoding. My goal is to keep this small.

The heart of the whole thing is some jQuery that ties everything together, here’s the code. Instead of going through all of the code, let’s just go over a few key points:

The heavy lifting is done in the getLogData method.

function getLogData(numRows, logFile, isTail) {
    var proxy = new ServiceProxy("ViewLog.aspx/");
    proxy.isWcf = false;
    proxy.invoke("GetLogTail", { logname: logFile, numrows: numRows },
        function (data) {
            displayLog(logFile, data);
            if (isTail) {
                var method = "getLogData(" + getNumRows() + ", '" + getLogFileName() + "', "+ isTail +")";
                logTailId = setTimeout(method, getNumSecs());
            }
        },
        function (errmsg) {
            clearTimeout(logTailId);
            alert(errmsg);
        },
        false);        // NOT bare
}

As you can see, there’s not much to it. By setting “isWcf = false” and passing in false for “bare” setting, the heaving lifting is done by the serviceproxy. if successful, then display the data and call “setTimeout” for the next ajax call. Okay, so I can hear you asking, “Why not use setInterval?”. For 2 reasons, I decided to useSetTimeout: first, the precision isn’t important, and second, if the ajax method is slow, I don’t want to hammer the web set for log reading. That’s all.

If there’s an error, clearTimeout is called, so we don’t keep calling the ajax method when things to bad.

displayLog is mostly

var $log = $('#logDiv');
var logStr = "";
for (var property in data) {
    logStr += formatLine(data[property]);
}
$log.html(logStr);

So, you probably would like to see the magic that is formatLine…

function formatLine(line) {
    line = line.encodeHtml();
    if (line.toLowerCase().indexOf('info') === 0) {
        return "<span class='info'>" + line + "</span><br/>";
    } else if (line.toLowerCase().indexOf('error') === 0) {
        return "<span class='error'>" + line + "</span><br/>";
    } else if (line.toLowerCase().indexOf('warn') === 0) {
        return "<span class='warn'>" + line + "</span><br/>";
    } else if (line.toLowerCase().indexOf('debug') === 0) {
        return "<span class='debug'>" + line + "</span><br/>";
    }
    return "<span>" + line + "</span><br/>";
}

The only interesting part of this is the “encodeHtml” method, which is one of the extension methods.

So when it is all done, this is how it looks. To turn on the “tail” operation, click tail; that’s about it.log1

About these ads

3 Responses to Website Log Tail

  1. francisco says:

    hola, he dado vueltas por la red y no he encontrado algo parecido y soy un buen programador a la pirmera, si pudieras explicarme un poco mas , o quizas un ejemplo sencillo de de el log tail. gracias .

    saludos francisco

  2. baji says:

    Very nice article…thank u so much….

    getting error in aspx.page where 1)MasterPageFile=”~/Admin.master”
    2)

    Any idea?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: