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.