2

I'm trying to show log contents to the users of my Swing application on demand. Displaying a list of log events, possibly allowing them to see details about log messages, ideally something similar to what the eclipse "Error Log" view displays.

Are there any premade Log display components/log appenders for that?

I realize that I could easily append the String values to a Textarea but if possible I'd like more fancyness "for free" (searching, filtering by event, filtering by logger, export, etc.)

I'm currently using SLF4J with logback but I don't have any problems with switching to another logging framework if such a component exist for it.

Patrick Huy
  • 975
  • 1
  • 8
  • 19

2 Answers2

1

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:

Community
  • 1
  • 1
Adam Mackler
  • 1,980
  • 1
  • 18
  • 32
0

I would look at SwingX. It has a nice demo at this page. Try JXTable for example.

Nándor Krácser
  • 1,128
  • 8
  • 13