How link log from Logback in Swing application?

2020-01-29 17:29发布

问题:

I have to add a panel to an application, this panel will log the errors of the application. I have create a class wich extends AppenderBase and i have configure the xml file to use this class.

So when i log somehting in the application, the appender he's called.

But at the moment i don't know how to link my appender to my panel.

Can you guide me please ?

回答1:

For this you have to write your custom Appender, for this see :logback.qos.ch/manual/appenders.html. Then you need to using a component such as JTextArea for showing log in it. Then you have to write a Programmatic Configuration logback for relation between your custom Appender and your Swing-Component. see :logback.qos.ch/manual/joran.html



回答2:

Working solution:

Custom appender class:

package br.com.mobhub.fdv.sync.utils;

import br.com.mobhub.fdv.sync.App;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;

/**
 * @author Rodrigo Garcia Lima (email: rodgarcialima@gmail.com | github: rodgarcialima)
 * @see ch.qos.logback.core.AppenderBase
 */
public class Appender extends AppenderBase<ILoggingEvent> {

    /**
     * Utilizo para formatar a mensagem de log
     */
    private PatternLayout patternLayout;

    /**
     * Cada nível de log tem um estilo próprio
     */
    private static SimpleAttributeSet ERROR_ATT, WARN_ATT, INFO_ATT, DEBUG_ATT, TRACE_ATT, RESTO_ATT;

    /**
     * Definição dos estilos de log
     */
    static {
        // ERROR
        ERROR_ATT = new SimpleAttributeSet();
        ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.TRUE);
        ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE);
        ERROR_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 0, 0));

        // WARN
        WARN_ATT = new SimpleAttributeSet();
        WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
        WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE);
        WARN_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 76, 0));

        // INFO
        INFO_ATT = new SimpleAttributeSet();
        INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
        INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.FALSE);
        INFO_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(0, 0, 153));

        // DEBUG
        DEBUG_ATT = new SimpleAttributeSet();
        DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
        DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE);
        DEBUG_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(64, 64, 64));

        // TRACE
        TRACE_ATT = new SimpleAttributeSet();
        TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
        TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE);
        TRACE_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(153, 0, 76));

        // RESTO
        RESTO_ATT = new SimpleAttributeSet();
        RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Bold, Boolean.FALSE);
        RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Italic, Boolean.TRUE);
        RESTO_ATT.addAttribute(StyleConstants.CharacterConstants.Foreground, new Color(0, 0, 0));
    }

    @Override
    public void start() {
        patternLayout = new PatternLayout();
        patternLayout.setContext(getContext());
        patternLayout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
        patternLayout.start();

        super.start();
    }

    @Override
    protected void append(ILoggingEvent event) {
        // Formata mensagem do log
        String formattedMsg = patternLayout.doLayout(event);

        // Forma segura de atualizar o JTextpane
        SwingUtilities.invokeLater(() -> {
            // Alias para o JTextPane no frame da aplicação
            JTextPane textPane = App.MAIN_FORM.getTextPane();

            try {
                // Trunca linhas para economizar memória
                // Quando atingir 2000 linhas, eu quero que
                // apague as 500 primeiras linhas
                int limite = 1000;
                int apaga = 200;
                if (textPane.getDocument().getDefaultRootElement().getElementCount() > limite) {
                    int end = getLineEndOffset(textPane, apaga);
                    replaceRange(textPane, null, 0, end);
                }

                // Decide qual atributo (estilo) devo usar de acordo com o nível o log
                if (event.getLevel() == Level.ERROR)
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, ERROR_ATT);
                else if (event.getLevel() == Level.WARN)
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, WARN_ATT);
                else if (event.getLevel() == Level.INFO)
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, INFO_ATT);
                else if (event.getLevel() == Level.DEBUG)
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, DEBUG_ATT);
                else if (event.getLevel() == Level.TRACE)
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, TRACE_ATT);
                else
                    textPane.getDocument().insertString(textPane.getDocument().getLength(), formattedMsg, RESTO_ATT);

            } catch (BadLocationException e) {
                // Faz nada
            }

            // Vai para a última linha
            textPane.setCaretPosition(textPane.getDocument().getLength());
        });
    }

    /**
     * Código copiado do {@link JTextArea#getLineCount()}
     * @param textPane de onde quero as linhas contadas
     * @return quantidade de linhas &gt; 0
     */
    private int getLineCount(JTextPane textPane) {
        return textPane.getDocument().getDefaultRootElement().getElementCount();
    }

    /**
     * Código copiado do {@link JTextArea#getLineEndOffset(int)}
     * @param textPane de onde quero o offset
     * @param line the line &gt;= 0
     * @return the offset &gt;= 0
     * @throws BadLocationException Thrown if the line is
     * less than zero or greater or equal to the number of
     * lines contained in the document (as reported by
     * getLineCount)
     */
    private int getLineEndOffset(JTextPane textPane, int line) throws BadLocationException {
        int lineCount = getLineCount(textPane);
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= lineCount) {
            throw new BadLocationException("No such line", textPane.getDocument().getLength()+1);
        } else {
            Element map = textPane.getDocument().getDefaultRootElement();
            Element lineElem = map.getElement(line);
            int endOffset = lineElem.getEndOffset();
            // hide the implicit break at the end of the document
            return ((line == lineCount - 1) ? (endOffset - 1) : endOffset);
        }
    }

    /**
     * Código copiado do {@link JTextArea#replaceRange(String, int, int)}<br>
     *
     * Replaces text from the indicated start to end position with the
     * new text specified.  Does nothing if the model is null.  Simply
     * does a delete if the new string is null or empty.<br>
     *
     * @param textPane de onde quero substituir o texto
     * @param str the text to use as the replacement
     * @param start the start position &gt;= 0
     * @param end the end position &gt;= start
     * @exception IllegalArgumentException if part of the range is an invalid position in the model
     */
    private void replaceRange(JTextPane textPane, String str, int start, int end) throws IllegalArgumentException {
        if (end < start) {
            throw new IllegalArgumentException("end before start");
        }
        Document doc = textPane.getDocument();
        if (doc != null) {
            try {
                if (doc instanceof AbstractDocument) {
                    ((AbstractDocument)doc).replace(start, end - start, str, null);
                }
                else {
                    doc.remove(start, end - start);
                    doc.insertString(start, str, null);
                }
            } catch (BadLocationException e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        }
    }
}

JTextPane instance example:

public class App {

    private static final Logger  logger = LoggerFactory.getLogger(App.class);
    public static final MainForm MAIN_FORM;

    static {
        // Look and Feel
        try {
            UIManager.setLookAndFeel(new NimbusLookAndFeel());
        } catch (UnsupportedLookAndFeelException e) {
            logger.error("Erro ao configurar NimbusLookAndFeel");
        }

        // Esse painel do form principal está sendo usando em outros lugares da aplicação
        MAIN_FORM = new MainForm();
    }

    public static void main(String[] args) {
        ...

        // Chama o form principal
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Força de Vendas (Sync)");
            frame.setJMenuBar(criaMenus(frame));
            frame.setContentPane(MAIN_FORM.$$$getRootComponent$$$());
            frame.setResizable(true);
            frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });

        ...
    }
    ...
}

And:

public class MainForm {

    private static final Logger logger = LoggerFactory.getLogger(MainForm.class);

    private JPanel contentPanel;
    private JButton iniciarButton;
    private JTextPane textPane;
    private JButton pararButton;
    private JButton limparLogButton;

    ...

    public JTextPane getTextPane() {
        return textPane;
    }

    ...

}

logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property file="config.properties" />

    <!-- Console -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logging.path}${logging.file}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logging.path}${logging.file}-%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>${logging.maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
        </encoder>
    </appender>

    <!-- Form -->
    <appender name="FORM" class="br.com.mobhub.fdv.sync.utils.Appender" />

    <!--<logger name="br.com.mobhub.fdv.sync.App" level="DEBUG"/>-->

    <root level="${logging.level}">
        <appender-ref ref="FILE" />
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FORM" />
    </root>

</configuration>