Tail a log file in the web browser
December 12, 2007 20:30:01 Last update: December 12, 2007 20:32:23
This is a script to tail a log file through the web browser. It uses AJAX, apache web server, mod_python, UNIX utilities
Although it's written in python, it should be easy to port to other languages such as Perl.
Apache
Python script:
tail (requires the --lines switch) and wc. The log file may reside on the web server or any other host accessible from the web server through SSH.
Although it's written in python, it should be easy to port to other languages such as Perl.
Apache
httpd.conf:
LoadModule python_module modules/mod_python.so # Log viewer Alias /log_viewer "C:/work/log_viewer" <Directory "C:/work/log_viewer"> SetHandler mod_python PythonHandler mod_python.publisher DirectoryIndex index.py Options FollowSymlinks AllowOverride None Order deny,allow Allow from all Satisfy all </Directory>
Python script:
import time, os from os.path import basename # command line utilities SSH = 'C:/local/bin/plink.exe -C %(host)s -P %(port)s -l %(userid)s -pw %(passwd)s "%(cmd)s"' LOCAL = '%(cmd)s' CAT = 'cat %s' WC = 'wc -l %s' TAIL = 'wc -l %s && tail --lines=+%s %s' # initial number of lines to show INITIAL_LINES = 100 APPS_LIST = [ 'local_app', 'remote_app', ] APPS = { 'local_app' : { 'name' : 'My Application - Local', 'file' : 'C:/logs/myapp/mylog.%(date)s.txt', 'shell' : LOCAL, 'host' : 'localhost', }, 'remote_app' : { 'name' : 'My Application - Remote', 'file' : '/home/myapp/mylog.%(date)s.txt', 'shell' : SSH, 'host' : 'apphost', 'port' : 22, 'userid' : 'userid', 'passwd' : 'passwd', }, } def index(app = None): appInfo = APPS.get(app) if appInfo: file = appInfo['file'] % { 'date' : time.strftime('%Y-%m-%d') } else: file = None return ''.join((html_header(app, file), html_footer())) def download_log(req, app = None, file = None): appInfo = APPS.get(app) if not appInfo: return req.content_type = 'application/octet-stream' req.headers_out['Content-Disposition'] = 'attachment; filename=%s' % basename(file) appInfo['cmd'] = CAT % file cmd = appInfo['shell'] % appInfo f = os.popen(cmd) for line in f: req.write(line) f.close() def get_log(req, app = None, file = None, start_line = 1): try: start_line = int(start_line) if start_line < 1: start_line = 1 except: start_line = 1 (new_line, payload) = __get_log(app, file, start_line) req.content_type = 'text/xml' req.no_cache = True req.no_local_copy = True return """<?xml version="1.0" encoding="utf-8"?> <ajax-response startLine="%s"> <![CDATA[%s]]> </ajax-response> """ % (new_line, payload) def __get_log(app, file, start_line): appInfo = APPS.get(app) if not appInfo: return (1, '') if start_line == 1: appInfo['cmd'] = WC % file cmd = appInfo['shell'] % appInfo f = os.popen(cmd) lineCount = f.read() if f.close(): return (1, '<span class="error">[ERROR] Failed to open file: %s:%s</span>' % (appInfo['host'], file)) try: lineCount = int(lineCount.split()[0]) except: return (1, '<span class="error">[ERROR] Failed to get a line count of: %s:%s (%s)</span>' % (appInfo['host'], file, lineCount)) start_line = lineCount > INITIAL_LINES and (lineCount - INITIAL_LINES) or 1 appInfo['cmd'] = TAIL % (file, start_line, file) cmd = appInfo['shell'] % appInfo f = os.popen(cmd) lineCount = f.readline() try: lineCount = int(lineCount.split()[0]) except: return (1, '<span class="error">[ERROR] Failed to get a line count of: %s:%s (%s)</span>' % (appInfo['host'], file, lineCount)) if start_line > lineCount: start_line = 1 lines = [] log_level = '' for line in f.readlines(): if line.find('[DEBUG') >= 0: line = '<span class="debug">%s</span><br/>' % __escape(line.rstrip()) log_level = 'DEBUG' elif line.find('[INFO') >= 0: line = '<span class="info">%s</span><br/>' % __escape(line.rstrip()) log_level = 'INFO' elif line.find('[WARN') >= 0: line = '<span class="warn">%s</span><br/>' % __escape(line.rstrip()) log_level = 'WARN' elif line.find('[ERROR') >= 0: line = '<span class="error">%s</span><br/>' % __escape(line.rstrip()) log_level = 'ERROR' elif log_level == 'WARN' or log_level == 'ERROR': line = '<span class="%s">%s</span><br/>' % (log_level, __escape(line.rstrip())) else: line = '<span>%s</span><br/>' % __escape(line.rstrip()) lines.append(line) start_line += 1 if f.close(): return (1, '<span class="error">[ERROR] Failed to read log file: %s:%s</span>' % (appInfo['host'], file)) else: return (start_line, ''.join(lines)) def __escape(html): return html.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') def html_header(theApp, file): # generate links links = [] for app in APPS_LIST: links.append('<a href="./?app=%s">%s</a>' % (app, APPS[app]['name'])) # set title, header and download link title = APPS.get(theApp) and APPS[theApp]['name'] or 'View Log Files' header = APPS.get(theApp) and APPS[theApp]['host'] + ': ' + file or '' downloadLink = APPS.get(theApp) and '<a href="download_log?app=%s&file=%s" title="Download"><img src="/download.gif" alt="Download" border="none"/></a>' % (theApp, file) or '' return """ <html> <head> <title>%s</title> <style> body { font-family: verdana, arial, sans-serif; } #loglinks { float: left; width: 100%%; margin-bottom: 20px; } #loglinks a { font-size: 0.85em; white-space: nowrap; } #log { color: #0000ff; } #log .debug { color: #000000; } #log .info { color: #208920; } #log .warn { color: #9c20ee; } #log .error { color: #ac2020; } #pause-img { position: absolute; top: -100px; left: 0; cursor: pointer; z-index: 1; } #play-img { position: absolute; top: -100px; left: 0; cursor: pointer; z-index: 0; } </style> <script language="JavaScript"> function ajaxGet(url, callback) { httpGet(url, function(xmldom) { var resp = xmldom.firstChild; while (resp) { if (resp.nodeName == 'ajax-response') { break; } resp = resp.nextSibling; } var retVal = new Object(); if (resp) { var attributes = resp.attributes; for (var i = 0; i < attributes.length; i++) { var attr = attributes.item(i); retVal[attr.name] = attr.value; } var child = resp.firstChild; while (child) { if (child.nodeName == '#cdata-section') { retVal.data = child.data; break; } child = child.nextSibling; } } callback(retVal); }); } function httpGet(url, callback) { var http = getXMLHTTP(); http.open("GET", url, true); http.onreadystatechange = function() { if (http.readyState == 4) { var xml = http.responseXML; if (xml && xml.firstChild) { callback(xml); } else { callback(http.responseText); } } }; http.send(null); } function getXMLHTTP() { var xmlhttp; try { xmlhttp = new XMLHttpRequest(); } catch (e) { try { xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { throw "Unable to create an HTTP Request object"; } } return xmlhttp; } var tail = true; function refreshLog() { var theApp = document.forms[0].theApp; var theFile = document.forms[0].theFile; var startLine = document.forms[0].startLine; if (! tail) { window.setTimeout(refreshLog, 5000); return; } ajaxGet('get_log?app=' + theApp.value + '&file=' + theFile.value + '&start_line=' + startLine.value, function(resp) { if (resp.data && resp.data.length > 0) { startLine.value = resp.startLine; var newLines = document.createElement('DIV'); newLines.innerHTML = resp.data; document.getElementById('log').appendChild(newLines); if (document.body.scrollHeight > document.body.clientHeight) { document.body.scrollTop = document.body.scrollHeight - document.body.clientHeight; } var pause = document.getElementById("pause-img"); var play = document.getElementById("play-img"); pause.style.top = (document.body.scrollHeight - 22) + 'px'; pause.style.left = (document.body.clientWidth - 22) + 'px'; play.style.top = (document.body.scrollHeight - 22) + 'px'; play.style.left = (document.body.clientWidth - 22) + 'px'; } window.setTimeout(refreshLog, 5000); } ); } function stopRefresh() { var pause = document.getElementById("pause-img"); pause.style.display = 'none'; tail = false; } function resumeRefresh() { var pause = document.getElementById("pause-img"); pause.style.display = ''; tail = true; } function handleScroll() { var pause = document.getElementById("pause-img"); var play = document.getElementById("play-img"); var left = document.body.clientWidth + document.body.scrollLeft - 22; pause.style.left = left + 'px'; play.style.left = left + 'px'; } window.onscroll = handleScroll; </script> </head> <body onload="refreshLog();"> <div id="loglinks">%s</div> <h3 id="header">%s %s</h3> <form> <input type="hidden" name="theApp" value="%s"/> <input type="hidden" name="theFile" value="%s"/> <input type="hidden" name="startLine" value="1"/> </form> <div> <pre id="log">""" % (title, ' | '.join(links), header, downloadLink, theApp, file) def html_footer(): return """</pre></div> <img id="pause-img" src="/pause.gif" title="Pause" onclick="stopRefresh();"/> <img id="play-img" src="/play.gif" title="Continue" onclick="resumeRefresh();"/> </body></html>"""