Tuesday, August 23, 2011

Log file tailer using PHP and MooTools

We've been working on a big project wherein my part is writing web services that are the interface between an app on a mobile device and our database. I found myself remote connecting to our 2 web servers and running baretail or wintail (both great) and looking at the PHP error logs, then back to my machine to do code fixes, etc.

I really wanted to have a webpage that would let me watch log files on whatever web server. I did a google search for : php log file tail : and found some useful ideas, but none really did what I wanted. I cobbled together a solution I really like.

The page looks for all the file in your PHP log folder and puts them in a drop down. When you select one, a mootools request (ajax) is called that initially grabs the last 50 lines and then polls every 2 seconds for any new lines. You can toggle whether the scrolling is "locked" at the bottom of the div (you always see the new lines) or floats. Useful if you want to look further up the list and don't want to have the scroll snatched down to the bottom while doing so. Finally, I added a little "..." that tracks along the bottom so I know it's working.

I am using MooTools 1.3.1 and I think just the mootools core will work.

Here are the files...

index.php
<?php
$logDir = 'C:\PHP\logs';
$dh = opendir($logDir);
while (false !== ($filename = readdir($dh))) {
if (! preg_match('/^\.{1,2}/', $filename))
$logFiles[] = $filename;
}
sort($logFiles);
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<link rel="stylesheet" type="text/css" href="logtail.css" />
<script type="text/javascript" src="http://<?php echo $_SERVER['SERVER_NAME']; ?>/scripts/mootools.js"></script>
<script type="text/javascript" src="logtail.js"></script>
</head>
<body>
<div style="padding: 10px;">
<h3 style="margin: 0 0 10px 0;">LogTail Viewer</h3>
<select id="logList" onchange="startTailLog();">
<option value="">Select...</option>
<?php foreach ($logFiles as $name) { ?>
<option value="<?php echo $name; ?>"><?php echo $name; ?></option>
<?php } ?>
</select>
<div id="errorDiv"></div>
<div id="logDiv"></div>
<div style="width: 90%;">
<div id="progressDiv" style="float: left;"></div>
<div style="float: right;"><button id="scrollLock" class="btn" onclick="toggleScrollLock();">Locked</button></div>
</div>
</div>
</body>
</html>

logtail.css
@CHARSET "UTF-8";

html, body {
font-family: Verdana, Arial, Helvetica, Sans-Serif;
font-size: 1.0em;
margin: 0;
padding: 0;
}

.btn {
font-size: 1.0em;
color: #004000;
border: 2px outset #568f56;
}

b {
color: #004000;
font-weight: bold;
}

#errorDiv {
color: red;
margin: 10px;
}

#logDiv {
width: 90%;
height: 600px;
margin: 10px 0 0;
padding: 10px;
border: solid 1px black;
font-size: 0.7em;;
text-align: left;
clear: both;
overflow: auto;
}

logtail.js
var logRequest;
var logLastSize = 0;
var timer;
var timerInterval = 3000;
var progressString = '';
var scrollLocked = true;

// Reset on starting a new log
function startTailLog() {
logLastSize = 0;
$('logDiv').set('html', '');
$('progressDiv').set('html', '');
$('errorDiv').set('html', '');
clearTimeout(timer);
tailLog();
}

var tailLog = function() {
logRequest.setOptions({data: {'logname' : $('logList').getSelected().getProperty('value')[0], 'logsize' : logLastSize}}).send();
};

// Toggle the scrolling
function toggleScrollLock() {
if (scrollLocked) {
scrollLocked = false;
$('scrollLock').set('html', 'Unlocked');
} else {
scrollLocked = true;
$('logDiv').scrollTo(0,$('logDiv').getScrollSize().y);
$('scrollLock').set('html', 'Locked');
}
}

/* Script to run when the DOM is loaded and ready */
window.addEvent('domready', function() {
logRequest = new Request.JSON( {
method : 'post',
async : true,
url : 'logtail.php',
data: { 'logname' : '',
'logsize' : 0},
onRequest : function() {
$('errorDiv').set('html', '');
},
onSuccess : function(response) {
if (response.result == true) {
var logLines = response.logtxt.split("\n");
var logLength = logLines.length;
if (response.logsize == 0) {
$('errorDiv').set('html', 'Empty log...');
$('logDiv').set('html', '');
} else {
for (i = 0; i < logLength - 1; i++) {
new Element('span').set('html', logLines[i] + "<br/>\n").inject($('logDiv'));
}
}
logLastSize = response.logsize;
progressString = $('progressDiv').get('html');
if (progressString.length < 10) {
$('progressDiv').set('html', progressString + '.');
} else {
$('progressDiv').set('html', '.');
}
if (scrollLocked && logLength > 1) {
$('logDiv').scrollTo(0,$('logDiv').getScrollSize().y);
}
timer = tailLog.delay(timerInterval);
}
else {
$('errorDiv').set('html', response.errorMsg);
$('progressDiv').set('html', '');
}
return true;
}
});
});

logtail.php
<?php
include_once ("global_CDC.php");

// Return Values array
$returnVals = array ();
$returnVals ['result'] = true;
$returnVals ['errorMsg'] = '';
$returnVals ['logsize'] = 0;
$returnVals ['logtxt'] = '';

$logDir = 'C:\PHP\logs';
$initialNumLines = 50;

$logName = filter_input ( INPUT_POST, 'logname', FILTER_SANITIZE_STRING );
if (($logName == FALSE || is_null ( $logName ))) {
$returnVals ['result'] = false;
$returnVals ['errorMsg'] = 'No log name supplied.';
echo json_encode ( $returnVals );
exit ();
}

$logLastSize = filter_input ( INPUT_POST, 'logsize', FILTER_VALIDATE_INT );

$logFile = $logDir . '\\' . $logName;
$currentSize = filesize ( $logFile );

// If the log file is empty, indicate that
if ($currentSize == 0) {
$returnVals ['logsize'] = $currentSize;
echo json_encode ( $returnVals );
exit ();
}

// If this is our first call to logtail, get the last N lines
if ($logLastSize == 0) {
$handle = fopen ( $logFile, "r" );
$linecounter = $initialNumLines;
$pos = - 2;
$beginning = false;
$text = array ();
while ( $linecounter > 0 ) {
$t = " ";
while ( $t != "\n" ) {
if (fseek ( $handle, $pos, SEEK_END ) == - 1) {
$beginning = true;
break;
}
$t = fgetc ( $handle );
$pos --;
}
$linecounter --;
if ($beginning)
rewind ( $handle );
$text [$initialNumLines - $linecounter - 1] = fgets ( $handle );
if ($beginning)
break;
}
fclose ( $handle );
$text = array_reverse($text);
$returnVals ['logtxt'] = implode("", $text);
} elseif ($currentSize == $logLastSize) {
// The file hasn't changed...
} else {
$text = array ();
$handle = fopen ( $logFile, "r" );
fseek($handle, $logLastSize);
while (! feof($handle)) {
$text[] = fgets($handle);
}
fclose($handle);
$returnVals ['logtxt'] = implode("", $text);
}
$returnVals ['logsize'] = $currentSize;
echo json_encode ( $returnVals );
exit();
?>

Display programming code in a blogger post

As I was composing my first blog post (which is now the second one), I realized I needed to learn how to put programming code into a Blogger post.

After trying a bunch, I found this link had the solution I liked the best : http://www.bloggergeeze.com/2010/02/how-to-create-code-block-on-blogger.html.

The HTML encoder worked well for PHP, HTML, CSS and Javascript.

I modified their css a bit to better suit my tastes.
.post code {
  width: 550px;
  font-family: Courier;
  margin: 0;
  border: 1px solid #596;
  border-width: 1px 1px;
  padding: 5px;
  display: block;
}

And there you have it!