Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Log Server - Web Server for Logs

0.00/5 (No votes)
9 Jun 2010 1  
View your logs in the browser.

Log.Server.png

Introduction

In my last project, I needed to view logs that my application wrote. For reasons I do not mention here, I did not have access to the machine where the application was running. I needed to find a creative solution to overcome this limitation, and after many thoughts, I found one. A web server that will display the last lines of the file every 20 seconds.

Using the Demo Application

Use the "Start" and "Stop" buttons to start and stop the log server. The server listens on port 8080, so to show the index page with the list of monitored files, use the address: "http:\\127.0.0.1:8080".

To add a file to monitor, use the "Add" button. A new dialog will appear. You should provide the prefix of the URL and the path of the file to be monitored, and press "OK". The file should be added to the index page with links to pages with the last 10, 20, 50, and 100 lines. The address of those pages are in the following format: http://127.0.0.1:8080/main/last/20, where main is the provided prefix and 20 is number of the last line to display.

Those pages are refreshed every 20 seconds so we know what is going on with the file in real time. This may be very helpful to diagnose problems when we can not debug it or when do not have access to the machine.

Embedding the Server in C# Programs

Sometimes the better solution is to embed the log server in an application. For example, to add two files "main.log" and "error.log", we can use the following code:

LogServer _logServer = new LogServer();
FileMonitorListHandler _handler = new FileMonitorListHandler();
_logServer.AddHandler(_handler);
_handler.AddFile("main","main.log");
_handler.AddFile("error","error.log");
server.Start(8080);

LogServer is the web server that can display log files. First, we add the instance FileMonitorListHandler which is responsible for displaying the files list and the page files. To add a log file, the AddFile method should be used. This method takes the the prefix URL ("main" and "error") and maps it to the monitored file path ("main.log" and "error.log").

After the server starts on 8080, we can view the last lines in those files by using the following address: http://127.0.0.1:8080/main/last/20 and http://127.0.0.1:8080/error/last/20, where 20 is the maximum number of last lines to display.

The following sections explain how this is being accomplished.

Creating the Web Server

When the web server is started, a new thread will be created to listen to incoming HTTP requests. _stopEvent will allow us to gracefully stop the server when needed.

public bool Start(int port) {
  if (_thread != null) {
    return false;
  }
  Port = port;
  bool rc = false;
  try {
    _thread = new Thread(new ThreadStart(Listen));
    _stopEvent = new AutoResetEvent(false);
    _thread.Start();
    rc = true;
  } catch (Exception ee) {
    Logger.Info("{0}",ee.Message);
  }
  return rc;
}

The Listen thread will listen to the incoming requests. We wait it idle until _stopEvent is set or there is a new connection to handle. In the first situation, we simply break from the infinite loop which terminates the server listening thread and stops the server. In the second situation, we handle the request in a new thread by calling MainHandler.HandleInThread().

private void Listen() {
  _listener = new TcpListener(Port);
  _listener.Start();
  Logger.Info("Listening on {0}",Port);
  while (true) {
    IAsyncResult ar = _listener.BeginAcceptSocket(
        new AsyncCallback(delegate(IAsyncResult aa) { }),null);
    WaitHandle[] handles = new WaitHandle[] { _stopEvent, ar.AsyncWaitHandle };
    int reason = WaitHandle.WaitAny(handles);
    if (reason == 0) {
      break;
    }
    Socket socket = _listener.EndAcceptSocket(ar);
    if (socket.Connected) {
      Logger.Info("Client Connected from IP {0}",socket.RemoteEndPoint);
      MainHandler handler = new MainHandler(ref socket,ref _handlers);
      handler.HandleInThread();
    }
  }
  _thread = null;
}

The MainHandler is responsible for parsing the request and creating the response. Those objects will be delivered to the MainHandler.Handle for extra processing. When processing is done, the response is sent to the browser, the socket is closed, and the thread terminates.

public void Handle() {
  Request request = Request.GetRequest(ref _socket);
  Response response = new Response();
  Handle(ref request,ref response);
  response.Send(ref _socket);
  _socket.Close();
}

The Handlers

Each server has a list of handlers that can added using LogServer.AddHandle. The handlers implement the IHandler interface:

public interface IHandler {    
  HandleStatus Handle(ref Request request,ref Response response);
}

The method Handle takes two parameters: request and response. The method should add the desired functionality (for example, updating StatusCode, ContentType, or Data of the response object ) and returns the HandleStatus. This is used by MainHandler.Handle. MainHandler.Handle moves the request and response objects to handlers one by one until one of the handlers Handle return HandleStatus.Done. If HandleStatus.Next is returned, the next handler will be called.

public HandleStatus Handle(ref Request request,ref Response response) {
  foreach (IHandler handler in _handlers) {
    if ( handler.Handle(ref request,ref response) == HandleStatus.Done ) {
      break;
    }
  }
  return HandleStatus.Done;
}

FileMonitorHandler

The magic is done in FileMonitorHandler.Handle which displays the pages.

public HandleStatus Handle(ref Request request,ref Response response) {
  char[] sep = new char[1];
  sep[0] = '/';
  string[] parts = request.Uri.LocalPath.Split(sep, 
                           StringSplitOptions.RemoveEmptyEntries);
  if (parts.Length > 0 && parts[0] != _prefix) {
    return HandleStatus.Next;
  }
  string contents = String.Empty;
  if (parts.Length > 2 && parts[1] == "last") {
    int num = Int16.Parse(parts[2]);
    int count = 0;
    foreach( string line in GetLastLines(_filename,num)) {
      contents += String.Format("<p class='row{0} row'>{1}</p>",count % 2,line);
      count++;
    }
  }

  response.Data += String.Format(
    @"<html>
        <head>
          <title>{0} - {1}</title>
          <meta http-equiv='refresh' content='20'>
          <style>                
            body {{ font-size: 14px; }}
            h1 {{font-size: 16px; }}
            .row0 {{ background-color: #f0f0f0; }}
            .row1 {{ background-color: #ffff99; }}
            .row  {{ margin:0px; border: 1px #00f solid; padding:2px 10px;}}
          </style>
        </head>
        <body>
          <h1>{0} - {1}</h1>
          {2}
        </body>
      </html>",Path.GetFileName(_filename),DateTime.Now.ToString(),contents);
  return HandleStatus.Done;
}

First, we check if the request belongs to this handler. If this is the case, it finds out how many last lines to display. It then reads the last lines and then generates the HTML with this content and instructs the browser to refresh the page every 20 seconds (the meta command: <meta http-equiv='refresh' content='20'>).

The FileMonitorListHandler

This handler is responsible for display the index page with the monitored files, with links to their last line pages.

public HandleStatus Handle(ref Request request,ref Response response) {
  foreach (IHandler handler in _handlers) {
    if (handler.Handle(ref request,ref response) == HandleStatus.Done) {
      return HandleStatus.Done;
    }
  }
  StringBuilder fileList = new StringBuilder();
  foreach (FileMonitorHandler handler in _handlers) {
    fileList.Append("<tr>");
    fileList.Append(String.Format("<td>{0}</td>",handler.Prefix));
    fileList.Append(String.Format("<td><a href='http://www.codeproject.com" + 
       "/{0}/last/10'>Last 10</a> <a href='http://www.codeproject.com/" + 
       "{0}/last/20'>Last 20</a> <a href='http://www.codeproject.com/" + 
       "{0}/last/50'>Last 50</a> <a href='http://www.codeproject.com/" + 
       "{0}/last/100'>Last 100</a> ",handler.Prefix));
    fileList.Append("</tr>");
  }
  response.Data += String.Format(
    @"<html>
        <head>
          <title>Welcome to Log Server</title<
          <style<                
            body {{ font-size: 14px; }}
            h1 {{font-size: 16px; }}
            td,th {{ border: 1px solid #000000; 
                     text-align:center; padding: 0px 10px; }}
            th {{ background-color:#ffffbb;}}
            td {{ background-color:#ffffcc;}}
            table {{ border-collapse:collapse; }}

          </style<
        </head<
        <body<
          <p<Monintoring the following files : </p<
          <table<
          <tr<<th<Prefix</th< <th<Last Lines</th<<tr<
           {0}
          </table<
        </body<
      </html<",fileList);
    
    return HandleStatus.Done;
}

What Next?

Although the server does not provide all the luxury that ASP.NET provides, it has some advantages. The server is lightweight, very simple to use, and it is easy to embed it in any C# application.

Another advantage is the ability to extend the server to respond to new type requests. The only thing that needs to be done is implementing the IHandler interface and adding a new instance of it to the server using LogServer.AddHandler.

History

  • Version 1 - First version.
  • Version 2 - Added the Log Server demo application.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here