JavaFX log4j TextArea log appender

Today I decided to attempt to output my log files into a text area of a JavaFX application that I have been working on. I found several references to doing this while using commons logging, but when log4j is involed, these methods don't work because all logging is routed through the log4j appenders. After figuring this out, I began researching how to get output from log4j. 

After doing a little digging on Google, I ran across some instances of this being accomplished through using a custom log4j appender and configuring the log4j properties to output to the appender. I had to make some minor code changes to accomodate this structure, but in the end it worked out great.

To begin, I found an example of an appender to use. It's pretty straightforward and takes into consideration the multi-threaded nature of JavaFx applications, implementing runnable to avoid potentially overloading the application. The following is the code for my text area appender:

package org.verifications.console;

import javafx.application.Platform;
import javafx.scene.control.TextArea;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.spi.LoggingEvent;

/**
 *
 * @author Russell Shingleton <shingler@oclc.org>
 */
public class TextAreaAppender extends WriterAppender {

    private static volatile TextArea textArea = null;

    /**
     * Set the target TextArea for the logging information to appear.
     *
     * @param textArea
     */
    public static void setTextArea(final TextArea textArea) {
        TextAreaAppender.textArea = textArea;
    }

    /**
     * Format and then append the loggingEvent to the stored TextArea.
     *
     * @param loggingEvent
     */
    @Override
    public void append(final LoggingEvent loggingEvent) {
        final String message = this.layout.format(loggingEvent);

        // Append formatted message to text area using the Thread.
        try {
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (textArea != null) {
                            if (textArea.getText().length() == 0) {
                                textArea.setText(message);
                            } else {
                                textArea.selectEnd();
                                textArea.insertText(textArea.getText().length(),
                                        message);
                            }
                        }
                    } catch (final Throwable t) {
                        System.out.println("Unable to append log to text area: "
                                + t.getMessage());
                    }
                }
            });
        } catch (final IllegalStateException e) {
            // ignore case when the platform hasn't yet been iniitialized
        }
    }
}

Once this was in, I added logic to my main application class to add a new text area and feed it into the new text area appender inside the constructor:

public class MainApp extends Application {

    private final TextArea loggingView = new TextArea();

    public MainApp() {
        TextAreaAppender.setTextArea(loggingView);
    }

    @Override
    public void start(Stage stage) throws Exception {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(VerificationsConfig.class);
        ConsoleController consoleController = context.getBean(ConsoleController.class);
        AnchorPane console = ((AnchorPane) consoleController.getTopPane());
        Scene scene = new Scene((Parent) consoleController.getConsole());
        scene.getStylesheets().add("/styles/Styles.css");
        stage.setScene(scene);
        stage.setTitle("Verification Console");
        stage.show();
        setupLogginView();
        console.getChildren().add(loggingView);

    }

    /**
     * The main() method is ignored in correctly deployed JavaFX application.
     * main() serves only as fallback in case the application can not be
     * launched through deployment artifacts, e.g., in IDEs with limited FX
     * support. NetBeans ignores main().
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    private void setupLogginView() {
        loggingView.setLayoutX(17);
        loggingView.setLayoutY(64);
        loggingView.setPrefWidth(723);
        loggingView.setPrefHeight(170);
        loggingView.setWrapText(true);
        loggingView.appendText("Starting Application");
        loggingView.setEditable(false);
    }
}

The last thing to do was add the new appender to my log4j properties in to inform log4j to use it for output as well.

# Root logger option
log4j.rootLogger=INFO, stdout, gui

# Direct log messages to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

# Append the logs to the GUI
log4j.appender.gui = org.verifications.console.TextAreaAppender
# Formatting of the output:
log4j.appender.gui.layout=org.apache.log4j.PatternLayout
log4j.appender.gui.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n

With everything in place I was able to fire up the application and successfully get log output inside my text area.