Skip to content

Instantly share code, notes, and snippets.

@jroper
Created June 15, 2012 12:19
Show Gist options
  • Save jroper/2936195 to your computer and use it in GitHub Desktop.
Save jroper/2936195 to your computer and use it in GitHub Desktop.
Play 2.0 Comet log tailing
package util.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import play.libs.F;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Comet logging appender
*/
public class CometAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private static final int MAX_LOG_BUFFER = 20;
private final Set<F.Callback<ILoggingEvent>> listeners = Collections.newSetFromMap(
new ConcurrentHashMap<F.Callback<ILoggingEvent>, Boolean>());
private final ILoggingEvent[] buffer = new ILoggingEvent[MAX_LOG_BUFFER];
private final AtomicInteger pos = new AtomicInteger();
public CometAppender() {
INSTANCE = this;
}
@Override
protected void append(ILoggingEvent event) {
for (F.Callback<ILoggingEvent> listener: listeners) {
try {
listener.invoke(event);
} catch (Throwable throwable) {
// If the listener is throwing exceptions, remove it
listeners.remove(listener);
}
}
// store event in cyclic buffer
buffer[pos.getAndIncrement() % MAX_LOG_BUFFER] = event;
}
private static volatile CometAppender INSTANCE;
public static void addListener(F.Callback<ILoggingEvent> listener) {
if (INSTANCE != null) {
if (INSTANCE.listeners.size() < 10) {
int pos = INSTANCE.pos.get();
for (int i = pos; i < pos + MAX_LOG_BUFFER; i++) {
ILoggingEvent event = INSTANCE.buffer[i % MAX_LOG_BUFFER];
if (event != null) {
try {
listener.invoke(event);
} catch (Throwable throwable) {
// The listener is bad, don't use it
return;
}
}
}
INSTANCE.listeners.add(listener);
}
}
}
public static void removeListener(F.Callback<ILoggingEvent> listener) {
if (INSTANCE != null) {
INSTANCE.listeners.remove(listener);
}
}
}
@(time: String, log: ch.qos.logback.classic.spi.ILoggingEvent)
<li class="@log.getLevel.toString.toLowerCase">
<span class="time">@time</span>
<span class="level">@log.getLevel.toString</span>
<span class="logger">@log.getLoggerName</span>
<span class="thread">@log.getThreadName</span>
<span class="message">@log.getFormattedMessage</span>
@if(log.getThrowableProxy != null) {
<pre class="exception">@{ ch.qos.logback.classic.spi.ThrowableProxyUtil.asString(log.getThrowableProxy) }</pre>
}
<script>window.scrollTo(0, document.body.scrollHeight);</script>
</li>
package controllers;
import ch.qos.logback.classic.spi.ILoggingEvent;
import play.libs.F;
import play.mvc.Result;
import play.mvc.Results;
import play.mvc.Security;
import services.auth.AdminAuthenticator;
import util.logging.CometAppender;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Used for tailing logs
*/
@Security.Authenticated(AdminAuthenticator.class)
public class LogController extends Results {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss.SSSS");
public static Result tailLogs() {
Chunks<String> chunks = new StringChunks() {
@Override
public void onReady(final Out<String> out) {
// First, write out the main page template
out.write(views.html.log.logs.render().body());
// Create logging callback
final F.Callback<ILoggingEvent> callback = new F.Callback<ILoggingEvent>() {
@Override
public void invoke(ILoggingEvent event) throws Throwable {
out.write(views.html.log.log.render(dateFormat.format(new Date(event.getTimeStamp())), event).body());
}
};
// Add it as a listener to the appender
CometAppender.addListener(callback);
// Add disconnected listener to remove the listener from the appender
out.onDisconnected(new F.Callback0() {
@Override
public void invoke() throws Throwable {
CometAppender.removeListener(callback);
}
});
}
};
return ok(chunks).as("text/html");
}
}
<configuration>
<conversionRule conversionWord="coloredLevel" converterClass="play.api.Logger$ColoredLevel" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home}/logs/application.log</file>
<encoder>
<pattern>%date - [%level] - from %logger in %thread %n%message%n%xException%n</pattern>
</encoder>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%coloredLevel %logger{15} - %message%n%xException{5}</pattern>
</encoder>
</appender>
<appender name="COMET" class="util.logging.CometAppender">
</appender>
<logger name="play" level="INFO" />
<logger name="application" level="INFO" />
<root level="ERROR">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
<appender-ref ref="COMET" />
</root>
</configuration>
<!DOCTYPE html>
<html>
<head>
<title>Application Logs</title>
<style>
body {
background-color: black;
margin: 0;
padding: 20px;
color: lime;
font-family: Monaco, monospace;
font-size: 11px;
}
ul {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
margin: 0 5px;
padding: 0;
}
.time {
min-width: 100px;
display: block;
float: left;
color: white;
}
.level {
min-width: 50px;
display: block;
float: left;
}
.fatal .level {
color: #ff4500;
}
.error .level {
color: red;
}
.warn .level {
color: yellow;
}
.info .level {
color: #87ceeb;
}
.debug .level {
color: purple;
}
.logger {
min-width: 100px;
color: #6b8e23;
display: block;
float: left;
}
.thread {
min-width: 300px;
color: gray;
display: block;
float: left;
}
.message {
}
.exception {
font-family: Monaco, monospace;
}
</style>
</head>
<body>
<h2>Application Logs</h2>
<p>Note: This only shows the logs from one node, and when you restart Play, you'll need to stop loading
this page and hit refresh.</p>
<ul>
@* Intentionally left open, so logs can gradually append as they come *@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment