I took a stab at this. While it's been an instructional exercise for me, and
quite close to being fully-usable, there's still a couple missing necessities.
Perhaps someone can help with the final touches.
Background
This is part of a larger working application, so while I'm only
posting the code related to this question, there are some artifacts of
the application. The code is in Scala, but downgrading to Java ought
to be straightforward. I'm not including most of the import
directives;
hopefully it's clear what classes I'm referring to. I
believe the only two classes with conflicting simple class names are
scala.swing.Component
and java.awt.Component.
Overview
The code exists in the following files:
Main.scala
has the the code that creates the log window.
LogFrame
is the swing window that displays the log entries
LogModel
stores the logging data
AbstractBaseTableModel
is the superclass of LogModel
as well as
another class in my application not shown here.
TableAppender.scala
is what connects Swing to Logback.
- A one-line method that formats timestamps is in my package object because it
is used elsewhere in my application.
logback.xml
has one line activating my new appender
The Code
In Main.scala
the Frame that displays the log output is made
visible:
object Main extends swing.SwingApplication {
override def startup(args: Array[String]) {
val logFrame = LogFrame
if (logFrame.size == new Dimension(0,0)) logFrame.pack()
logFrame.visible = true
}
}
The LogFrame
singleton object is defined in LogFrame.scala
:
object LogFrame extends Frame {
title = "Log"
iconImage = new ImageIcon("log.png").getImage
preferredSize = new Dimension(1200,370)
object LogTable extends Table {
model = LogModel
Map(0 -> 50, 1 -> 32, 4 -> 400) foreach { m =>
peer.getColumnModel getColumn m._1 setPreferredWidth m._2
}
override def rendererComponent(
isSelected: Boolean, focused: Boolean, row: Int, column: Int
) = {
val v = model.getValueAt(
peer.convertRowIndexToModel(row),
peer.convertColumnIndexToModel(column)
).toString
TableCellRenderer.componentFor(this, isSelected, focused, v, row, column)
}
import ch.qos.logback.classic.Level.{ERROR,WARN}
object TableCellRenderer extends AbstractRenderer[String, TextArea](new TextArea {
lineWrap = true; wordWrap = true
}) {
val brown = new java.awt.Color(143,112,0)
def configure(t: Table, sel: Boolean, foc: Boolean, s: String, row: Int, col: Int) = {
component.text = s
model.getValueAt(
LogTable.this.peer.convertRowIndexToModel(row),
LogModel.columnNames.indexOf("Level")
) match {
case ERROR => component.foreground = java.awt.Color.RED
case WARN => component.foreground = brown
case _ =>
}
}
}
}
contents = new BoxPanel(Vertical) {
contents += new ScrollPane { viewportView = LogTable }
}
def logEvent(event: ILoggingEvent) {
event +=: LogModel
}
}
The LogModel
singleton is in LogModel.scala
.
object LogModel extends AbstractBaseTableModel {
final val columnNames = Array("Time","Level","Thread","Logger","Message")
val data = ListBuffer[Array[AnyRef]]()
def +=:(event: ILoggingEvent) {
Array[AnyRef](
formatTimeStamp(event.getTimeStamp),
event.getLevel,
event.getThreadName,
event.getLoggerName.replaceFirst(".*\\.",""),
event.getFormattedMessage
) +=: data
fireTableChanged( new TableModelEvent(this) )
}
}
These two lines in my package object define the
formatTimeStamp()
method:
final val isoFormatter = org.joda.time.format.ISODateTimeFormat.dateTimeNoMillis
def formatTimeStamp(millis: Long) = isoFormatter.print(millis)
The reason LogModel
extends AbstractBaseTableModel
is because my
application has some other table models that are updated all-at-once,
rather than one row at at time as the logging model is. Thus
AbstractBaseTableModel
has a data
member of type SeqLike
, and
the subclasses can use either an immutable List
or a mutable
ListBuffer
as desired.
import scala.collection.SeqLike
abstract class AbstractBaseTableModel extends AbstractTableModel {
val columnNames: Array[String]
val data: SeqLike[Array[AnyRef],_]
def getRowCount: Int = data.size
def getColumnCount: Int = columnNames.size
override def getColumnName(column: Int) = columnNames(column)
override def getValueAt(row: Int, column: Int): AnyRef = data(row)(column)
override def isCellEditable(row: Int, col: Int) = false
override def getColumnClass(columnIndex: Int): Class[_] =
getValueAt(0, columnIndex).getClass
}
The TableAppender.scala
file is small:
class TableAppender extends AppenderBase[ILoggingEvent] {
def append(event: ILoggingEvent) {
LogFrame.logEvent(event)
}
}
Finally, the change to my logback.xml
file is even smaller:
<appender name="swingTable" class="mypackage.TableAppender"/>
Issues
The only remaining problems that are preventing me from deciding this
is better than an old-fashioned log-file are:
1) Because I've overridden Table.rendererComponent()
,
JTable.setDefaultRenderer()
does not work as usual.
2) I haven't figured out how to get the wrapped lines to be visible in
the Table. All I know is that I have to increase the height of the
rows.
If those issues were resolved, the next thing would be to add
real-time filtering according to log level and logger names. As it
is, I'm going to set this aside for now at least, but if anyone
has any suggestions, I'm interested in hearing them.
References
The following resources were either useful to me in composing the foregoing
code, or might be useful in resolving the remaining issues: