So, I really wanted to create a demo for websockets that was not the norm. I wanted to try something that was more of a "what could I really do with with this" demo. After bouncing around a few ideas I finally settled on this, Log Watcher.

How many times have you wished you could tail a log file on your remote ColdFusion server? How many times have you remoted into your server to tail a log? If you are anything like me (probably not but for arguments sake lets say you are) you have probably done it once this week. At the very least once in the past few weeks.

What is log tailing? Well, to sum up it is viewing a log file as new lines are written to the file. This can be accomplished on unix using "tail -f {log}" or using something like BareTail on windows.

Now, what if you didn't have to remote into the server to tail the log? This is where Log Watcher comes in. Log Watcher allows you to tail a log via a web browser. This low overhead app does the same thing as a server based tail but with a little more flare.

So, how does it work? Well, it is all made possible due to ColdFusion 10. The interface is a simple browser app using Bootstrap for the pretty (I don't do pretty). The real guts of the app is on the server. Here, threading is used and a couple java objects to do a constant read on a file. The data (rows) from the log file are pushed to the client using websockets.

To accomplish this there are a few things I had to overcome. First was dealing with thread management. Creating threads that are designed to run forever is not something you generally want to do in a browser based application. This turned out be one of the more challenging parts. But this became infinitely easier thanks to a new setting in ColdFusion 10 to set the timeout to none.

view plain print about
1<cfsetting requesttimeout="0">

Next was dealing with the display and that all logs are not created equal. Then there was the issue of the app crashing the ColdFusion server (more on this later). A lot of trial and error went into building a way to display the log rows effectively. Once I solved these issues I was off to the races.

The client portion of the app is very basic. The only coolness there is the tiny amount of code for the client to subscribe to a web socket. Now, thanks to how websockets were implemented this results to just one tag

view plain print about
1<cfwebsocket name="lrbase" onmessage="wsFunctions.mycbHandler" subscribeto="lrbase"/>

This one little tag brings in all the necessary bits to do websockets. All we have to put in is the name of the websocket (as defined in applicaiton.cfc), the message handler, and the name of the channel to auto subscribe to on load.

Then just a little javascript to process the incoming messages. Here I handle the incoming data and split it up so I can display the data in a table. Name spacing my javascript has become a little habit. I don't do it all the time, but I find it useful when I want to encapsulate some code.

view plain print about
1var wsFunctions = (function() {
2
3    var wsResponsProcessor = function(obj){ // handle responses from WS
4
5        if (obj.type == 'data'){
6            
7            $('#mainContent2').show();            
8            output = splitMessage(obj.data);            
9            if (rowOut == 1 && useTableFormat(currentLog)){
10                    $('#headerRow').html(output);
11            
12            } else {
13            
14                if (loadDirection == 1){
15                    $(document).scrollTop($(document).height());                         
16                    $('#contentTable').append('<tr><td>'+rowOut+'</td>'+output+'</tr>');
17                } else {
18                    $('#contentTable').prepend('<tr><td>'+rowOut+'</td>'+output+'</tr>');
19                }    
20            }
21            rowOut++;
22        }    
23     
24 }
25    
26    return {
27        
28        mycbHandler: function(aEvent){
29         wsResponsProcessor(aEvent);
30        }
31    };    
32})();

To sum up all this javascript. If the type is "data" it processes the message. It is possible for the message to be other types for example the response message when subscribing. The message is then passed of to the splitMessage function. This function handles splitting the message into columns if the log is formatted that way. Then depending on load direction the row is either appended or prepended to the display.

To get the ball rolling there is a click handler on the log list. This processed the selected log and makes a ajax call to the server.

The logLoader.cfm does a few things. First, it includes the kill.cfm file. This file runs and sets a server var that any currently running thread will see and cause the thread to terminate. We then sleep the call for 1.5 seconds to give any other thread a chance to terminate. We then create the thread name and call the thread component.

The thread component is where the "magic" happens. This is where the thread is created and starts reading the requested log file.

view plain print about
1<cffunction name="readLog" access="public">
2        <cfargument name="threadName" type="string" required="true" />
3        <cfargument name="log" type="string" required="true" />
4        <cfargument name="channel" type="string" required="true" />
5        
6        
7        <cfthread action="run" name="#arguments.threadName#" logfile="#arguments.log#" channel="#arguments.channel#">
8        
9            <cfset server.channels["#attributes.channel#"] = thread.name>
10            <cfsetting requesttimeout="0">
11            <cfscript>                
12                FileName = "#server.logbase#/#attributes.logfile#.log";
13                FileIOClass = createObject("java", "java.io.FileReader");
14                FileIO = FileIOClass.init(FileName);                
15                LineIOClass = createObject("java", "java.io.BufferedReader");
16                LineIO = LineIOClass.init(FileIO);
17            
</cfscript>
18            <cfloop condition="1 eq 1">
19                <cfif server.channels["#attributes.channel#"] is not thread.name>
20                    <cfexit>
21                </cfif>        
22                
23                <cfset CurrLine = LineIO.readLine()>
24                
25                <cfif IsDefined("CurrLine")>                    
26                    <!---send websocket message--->
27                    <cfset wspublish("lrbase.#attributes.channel#", CurrLine)>
28                <cfelse>        
29                    <cfthread action="sleep" duration="1000" / >
30                </cfif>
31                    
32            </cfloop>
33        </cfthread>
34    </cffunction>

Walking through the function the first thing is does is create a thread with an action of run. This will create a running thread that will be come "detached" from the request that created it. First a server var is set with this threads name and then the timeout is set to 0 (infinity). Next a couple java objects are utilized to read the file. This will allow for a non-blocking read on the file. This type if read is desired so that the system can still write to the file while it is being read.

Next a loop is created that has a condition that will never end. Then in the loop a check is made to see if the server var for the active thread matches the current thread. If not the loop will exit thus also ending the thread. Next a line in the file is read. This will always read the next unread line. So, it will start at line 1 and get the next line on every loop. Eventually the currline var will not be defined. This is because the whole file is read. If the var is defined a websocket message is published to the channel for the app. This will magically make it back to the client and displayed.

Now, if the currline var is undefined the thread is put to sleep for a second. This is here because this is where the ColdFusion server was crashing. Not putting the sleep here caused CF to spike the processor up to 100% and eventually die. After some trial and error the 1 second sleep seemed to be the sweet spot. Now the thread was never over 2% cpu load.

As the loop continues over time any new line written to the file will be picked up and processed.

So there you have it. You can download the code (attached to this post) and check it out. Just unzip the code and drop it into your web root for ColdFusion 10.

Till next time...

--Dave