2

I have a Java logger that currently outputs to a file using the settings specified below. I also have a JavaFX TextArea and I want my logger to write to both the file and the TextArea concurrently.

Logger settings:

java.util.logging.FileHandler.level     = ALL
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.FileHandler.append    = true
java.util.logging.FileHandler.pattern   = log.txt

Logger declaration:

static Logger LOGGER;
static {
    try(FileInputStream ins = new FileInputStream("src/main/resources/log.config")) {
        LogManager.getLogManager().readConfiguration(ins);
        LOGGER = Logger.getLogger(BuildGet.class.getName());
        initialContext = new InitialContext();
    } catch (NamingException | IOException e) {
        LOGGER.log(Level.WARNING,"ERROR LOGGER", e);
    }
}

JavaFX TextArea:

```xml``<TextArea layoutX="20.0" layoutY="755.0" prefHeight="80.0" prefWidth="560.0" fx:id="logTextArea"/>`

An error occurs while playing the example:

javafx.fxml.LoadException: 
/C:/Users/slizo/Desktop/DConsole/target/classes/DConsoleScene.fxml:18

at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2707)
at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:944)
at javafx.fxml.FXMLLoader$InstanceDeclarationElement.processAttribute(FXMLLoader.java:981)
at javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:230)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:755)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2808)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2634)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2548)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2516)
at jmxClientConsole.gui.DConsoleFrame.start(DConsoleFrame.java:46)
at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(LauncherImpl.java:847)
at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$12(PlatformImpl.java:484)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.NoSuchMethodException: jmxClientConsole.DConsoleFrameControllerGetMessage.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3349)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2553)
at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:939)
... 17 more

Here is my startup class:

package jmxClientConsole.gui;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import jmxClientConsole.BuildGet;
import jmxClientConsole.TextAreaHandler;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;


public class ConsoleFrame extends Application {

static Logger LOGGER;
static {
    try(FileInputStream ins = new FileInputStream("src/main/resources/log.config")) {
        LogManager.getLogManager().readConfiguration(ins);
        LOGGER = Logger.getLogger(BuildGet.class.getName());
        LOGGER.setUseParentHandlers(false);
    } catch (IOException e) {
        LOGGER.log(Level.WARNING," Eror ConsoleFrame ", e);
    }
}
public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage stage)  {
    try {
        FXMLLoader loader = new FXMLLoader();
        URL xmlUrl = getClass().getResource("/ConsoleScene.fxml");
        loader.setLocation(xmlUrl);
        TextArea textArea = new TextArea();
        textArea.setEditable(false);
        textArea.setFont(Font.font("Monospaced", 13));

        TextAreaHandler handler = new TextAreaHandler(textArea);
        handler.setFormatter(new SimpleFormatter());
        LOGGER.addHandler(handler);

        Parent root = loader.load();
        stage.setTitle("Console");
        stage.getIcons().add(new Image(ConsoleFrame.class.getClassLoader().getResourceAsStream("logoC.png")));
        stage.setScene(new Scene(root));
        stage.show();
        LOGGER.log(Level.INFO,"Success console ");
    } catch (Exception e) {
        LOGGER.log(Level.SEVERE,"Eror Console", e);
    }
}
}

What I wrote in the comment about the controller, I had this. I have all the logic of the buttons in it:

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="900.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="jmxClientConsole.ConsoleFrameControllerGetMessage">
DarkSoul
  • 55
  • 6
  • What's wrong here? – DarkSoul Jun 16 '23 at 10:09
  • Maybe look into the StreamHandler class, especially the #setOutputStream method and #addHandler method of the Logger class. – SquidXTV Jun 16 '23 at 10:24
  • Use [this fx log util](https://stackoverflow.com/questions/24116858/most-efficient-way-to-log-messages-to-javafx-textarea-via-threads-with-simple-cu/24140252#24140252). – jewelsea Jun 16 '23 at 12:38
  • Extend [handler](https://docs.oracle.com/en/java/javase/17/docs/api/java.logging/java/util/logging/Handler.html). Implement [publish](https://docs.oracle.com/en/java/javase/17/docs/api/java.logging/java/util/logging/Handler.html#publish(java.util.logging.LogRecord)) to forward the published log record to the fx log util. [Add the handler](https://docs.oracle.com/en/java/javase/17/docs/api/java.logging/java/util/logging/Logger.html#addHandler(java.util.logging.Handler)) to your logger. – jewelsea Jun 16 '23 at 12:38
  • Your `FXML` appears to be invalid. Maybe it's a typo? `fx:id=""logTextArea` – SedJ601 Jun 16 '23 at 17:32
  • Thank you for tagging the imprint. I have updated my question – DarkSoul Jun 16 '23 at 18:37
  • Why did you edit the title to be non-english? Trying to get the question closed? – Olaf Kock Jun 16 '23 at 18:55
  • I have corrected my title. Accidentally displayed in Russian – DarkSoul Jun 16 '23 at 22:18

1 Answers1

4

Ultimately, you'll want a java.util.logging.Handler that appends the log messages to a TextArea. Here is a relatively simple implementation:

package sample;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.logging.ErrorManager;
import java.util.logging.Handler;
import java.util.logging.LogRecord;
import javafx.application.Platform;
import javafx.scene.control.TextArea;

public class TextAreaHandler extends Handler {

    // Avoid doing too much work at once. Tune this value as needed.
    private static final int MAX_APPEND = 100;

    private final Queue<LogRecord> recordQueue = new ArrayDeque<>();
    private boolean notify = true;

    private final TextArea textArea;
    private volatile boolean open = true;

    public TextAreaHandler(TextArea textArea) {
        this.textArea = textArea;
    }

    @Override
    public void publish(LogRecord record) {
        if (open && isLoggable(record)) {
            if (Platform.isFxApplicationThread()) {
                appendRecord(record);
            } else {
                synchronized (recordQueue) {
                    recordQueue.add(record);
                    if (notify) {
                        notify = false;
                        notifyFxThread();
                    }
                }
            }
        }
    }

    private void notifyFxThread() {
        try {
            Platform.runLater(this::processQueue);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.GENERIC_FAILURE);
        }
    }

    private void processQueue() {
        List<LogRecord> records = new ArrayList<>();
        synchronized (recordQueue) {
            while (!recordQueue.isEmpty() && records.size() < MAX_APPEND) {
                records.add(recordQueue.remove());
            }

            if (recordQueue.isEmpty()) {
                notify = true;
            } else {
                notifyFxThread();
            }
        }
        records.forEach(this::appendRecord);
    }

    private void appendRecord(LogRecord record) {
        String message;
        try {
            message = getFormatter().format(record);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.FORMAT_FAILURE);
            return;
        }

        try {
            textArea.appendText(message);
        } catch (Exception ex) {
            reportError(null, ex, ErrorManager.GENERIC_FAILURE);
        }
    }

    @Override
    public void flush() {
        /*
         * This implementation has no "buffer". If the 'recordQueue' is not
         * empty then the JavaFX Application Thread has already been notified
         * of work, meaning the queue will be emptied eventually. And if the
         * JavaFX framework has been shutdown, then there's no work to do
         * anyway.
         */
    }

    @Override
    public void close() {
        open = false;
    }
}

The above could be used like so:

package sample;

import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextArea;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {

    private static final Logger LOGGER = Logger.getLogger(Main.class.getName());

    static {
        LOGGER.setUseParentHandlers(false);
    }

    @Override
    public void start(Stage primaryStage) {
        TextArea textArea = new TextArea();
        textArea.setEditable(false);
        textArea.setFont(Font.font("Monospaced", 13));

        TextAreaHandler handler = new TextAreaHandler(textArea);
        handler.setFormatter(new SimpleFormatter());
        LOGGER.addHandler(handler);

        primaryStage.setScene(new Scene(textArea, 600, 400));
        primaryStage.show();

        LOGGER.info("This log message will be appended to the text area.");
    }
}

Unfortunately, I don't know how (if possible) you can configure this from a file. At least, not without some way to indirectly get the TextArea (e.g., dependency injection, JNDI(?), etc.).


You've since edited your question to say you're getting this error:

Caused by: java.lang.NoSuchMethodException: jmxClientConsole.DConsoleFrameControllerGetMessage.<init>()
     at java.base/java.lang.Class.getConstructor0(Class.java:3349)
     at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2553)
     at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:939)

This is a separate problem than what you were originally asking about. However, that error is telling you that some code is expecting your class (as specified in the error) to have a no-argument constructor, but that constructor doesn't exist.

If that class is functioning as an FXML controller, then note that the default controller factory needs a no-argument constructor. Either add that constructor or set a custom controller factory (how to do that is beyond the scope of this Q&A; there should be tutorials out there and probably other Stack Overflow Q&A's).

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • It's not very clear how to combine my controller class for fxml and textArea with the log. That is, I have a private TextArea in the controller class for processing buttons, and you add TextArea in the launch class – DarkSoul Jun 18 '23 at 19:30
  • The concept doesn't change. Just do the configuration in the controller instead. Or provide a method on the controller to get the text area. – Slaw Jun 18 '23 at 19:39
  • That changes your question. I suggest reverting the changes you made and then asking a new question. Especially since your error, `Caused by: java.lang.NoSuchMethodException: jmxClientConsole.DConsoleFrameControllerGetMessage.()`, has nothing to do with logging to a text area. That said, the error means some code expects your class to have a no-argument constructor, but that constructor is missing. – Slaw Jun 18 '23 at 20:04
  • I asked a new question with a clarification of my implementation. Now there are no errors, but logging is still not displayed in the TextArea: https://stackoverflow.com/questions/76502726/how-to-output-application-logs-dynamically-to-textarea-and-text-file?noredirect=1#comment134889604_76502726 – DarkSoul Jun 19 '23 at 06:19
  • I've made some updates to the `TextAreaHandler` to be more robust. Though those changes won't fix any additional problems you're having. Note in my answer I call `LOGGER.setUseParentHandlers(false)`. If you remove that line, then you'll also see the log output to the console, because the root `Logger` has a `ConsoleHandler` by default. So, assuming you have all loggers configured to use their parent logger's handlers, and you want _all_ messages to be appended to the text area, consider adding the `TextAreaHandler` to the root logger (`Logger.getLogger("")`). – Slaw Jun 19 '23 at 19:55
  • And, of course, any logs emitted before `TextAreaHandler` has been registered won't appear in the text area. There may be a way to change that, but the implementation would get much more complex. – Slaw Jun 19 '23 at 20:00