Log4Play: Log4j Live Streaming with Play Framework, Knockout.js and WebSockets

Log4Play is a Play! Framework module that provides a log4j appender which publishes log entries to an EventStream. In theory, you can use it from any Java application that uses log4j, but I have only tested it with a Play! Framework application. Log4Play provides an user interface which uses a WebSocket to create a live stream of log messages. The user interface allows you to tail the logs of your application without needing to login to the actual box. I worked on it with Deepthi Rallabandi, who I am working with on an Accenture project; I just introduced her to Play!. It has been a pleasant surprise to see how quickly she’s progressing, which again confirms how productive Play! is. In one day, she went from knowing nothing about WebSockets, and not that much Web experience, to having a full working WebSockets-based application with Play!. She used my previous WebSockets article to guide her through the process. So let me go over the implementation.

First we created a Log4J appender.

public class PlayWebSocketLogAppender extends WriterAppender implements Appender { /** * Publish log event to WebSocket Stream * * @see org.apache.log4j.WriterAppender#append(org.apache.log4j.spi.LoggingEvent) */ @Override public void append(LoggingEvent event) { LogStream.publish(new Log4PlayEvent(event)); } }

Then we created a plugin to add the appender to log4j automatically, so you don’t have to modify your log4j configuration. A PlayPlugin allows you to customize the behavior of the framework, I highly recommend you going through the source code.

public class Log4PlayPlugin extends PlayPlugin { /** * On application start. */ @Override public void onApplicationStart() { // Add appender that will stream log messages as Log4PlayEvent instances // through WebSocket (Log4Play.WebSocket.stream) PlayWebSocketLogAppender appender = new PlayWebSocketLogAppender(); Logger.log4j.addAppender(appender); // Add routes for the UI Router.addRoute("GET", "/@logs", "Log4Play.index"); Router.addRoute("WS", "/@logs/stream", "Log4Play.WebSocket.stream"); } }

Then we created an event stream which will be receiving the log messages from the appender.

public abstract class LogStream { /* The stream. */ public static final ArchivedEventStream stream = new ArchivedEventStream(50); /* * Gets the stream. * * @return the stream / public static EventStream getStream() { return stream.eventStream(); } /* * Publish. * * @param event * the event */ public static void publish(Log4PlayEvent event) { stream.publish(event); } }

Notice how we are using an ArchivedEventStream which we can use to display x numbers of messages as soon as the user loads the user interface, instead of seeing a blank page which will then display log messages as they happen from that point on. That’s the main difference between the ArchivedEventStream and the EventStream which I used on my first WebSockets article, WebSockets with Play Framework 1.2 in Action!.

Then we defined a WebSocketController which listens to messages dropped on the event stream and push them to the client.

public class Log4Play extends Controller { /* * Index. */ public static void index() { render(); } /* * The Class WebSocket. / public static class WebSocket extends WebSocketController { /* * Index. */ public static void index() { EventStream loggingStream = play.modules.log4play.LogStream.getStream(); while (inbound.isOpen()) { try { Promise promise = loggingStream.nextEvent(); play.modules.log4play.Log4PlayEvent event = await(promise); outbound.sendJson(event); } catch (Throwable t) { Logger.error(play.modules.log4play.ExceptionUtil.getStackTrace(t)); } } } } }

The difference between this implementation and the one from my previous article is that on this case a JSON object is getting sent to the view, instead of a plain string.

Then on the view side we used Knockout.js.

// Define Knockout.js Observable var viewModel = {}; viewModel.details = ko.observable(); ko.applyBindings(viewModel); viewModel.details("Log Events will start showing up here..."); // Create a socket var socket = new WebSocket('@@{Log4Play.WebSocket.index()}'); // Display a message var data = ""; var display = function(json) { var event = JSON.parse(json); if ( event != null && json != null ) { var checkLevel = document.getElementById("log4playlevel" + event.level); if ( checkLevel != null && checkLevel.checked == true ) { var item = event.level + ' - ' + event.category + ' - ' + event.date + ' - ' + event.message; data = item + data; viewModel.details(data); } } } // Message received on the socket socket.onmessage = function(event) { display(event.data); }


Under dependencies.yml:

require: – play → log4play 0.0.1

Under routes:

WS /logstream Log4Play.WebSocket.index GET /@logs Log4Play.index GET /publiclog4play/ staticDir:publiclog4play

Live Demo

A live demo is available at http://log4play.mashup.fm:9030/@logs. As soon as you enable the module on your application you should have the same UI available as well under /@logs.

Source Code

The source code is available on Github at https://github.com/feliperazeek/log4play.

Now Go Play!