4

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 ?

Shoxolat
  • 97
  • 2
  • 9
  • What do you want?! You don't discuss complete. – Sam Feb 22 '12 at 13:10
  • How can i dysplay my error log by org.slf4j.Logger in swing panel ? In the same application. – Shoxolat Feb 22 '12 at 13:11
  • 1
    You have to writing your custom `Appender`. for this see: http://logback.qos.ch/manual/appenders.html and you can help of traditionally question, here explain your goals in `Log4J`, `Logback` also same. – Sam Feb 22 '12 at 13:18
  • Yes i write my custom appender, but at this moment i don't know how to tell him to write in my pannel. – Shoxolat Feb 22 '12 at 13:20
  • 1
    Don't exist only a way, for sample you can add a 'JTextArea' and put your log into it from your appender. – Sam Feb 22 '12 at 13:25
  • Ok, but that i don't understand is : - i create my panel when i create my application - the appender is create with the xml file How can i tell to my appender to write in my JTextArea or something else in my panel ? I don't understand how link these clases. Sorry. :s – Shoxolat Feb 22 '12 at 13:28
  • You can use `Programmatic Configuration` logback, see : http://logback.qos.ch/manual/joran.html. This doing by classes such as : `JoranConfigurator`, `LoggerContext`, `StatusPrinter' and etc in `Logback`. – Sam Feb 22 '12 at 13:40
  • @Shoxlat You are respectful but my opinion is if help(comment or answer) of anyone be usefull for you, you better give `up vote` or `greate comment` to it ;) – Sam Feb 22 '12 at 14:00
  • @MJM how can i up vote comment ? :s – Shoxolat Feb 22 '12 at 14:10
  • In comment you can do this with `greate coment` icon ;) – Sam Feb 22 '12 at 14:11
  • @MJM put a summary of your comments in an answer and I would up-vote it. – Andrew Thompson Feb 22 '12 at 14:40
  • @ Andrew Thompson I thanks for your affection. – Sam Feb 22 '12 at 15:02
  • Example [here](http://stackoverflow.com/a/7682825/373489). – Adam Mackler Jan 19 '14 at 16:46

3 Answers3

12

Working solution:

enter image description here

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>
Rod Lima
  • 1,509
  • 26
  • 30
  • 1
    Thx for providing working solution -- SO much more useful than just "you need to go write a custom Appender" type answers. – Brian Oct 28 '20 at 19:30
3

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

Sam
  • 6,770
  • 7
  • 50
  • 91
0

The solution by @Rod Lima is cool, but the code is more complicated then needed. I ported it to Kotlin and simplified it in the process:

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 java.awt.Color
import javax.swing.JTextPane
import javax.swing.SwingUtilities
import javax.swing.text.SimpleAttributeSet
import javax.swing.text.StyleConstants

private fun createStyle(bold: Boolean = false, italic: Boolean = false, foreground: Color = Color.BLACK) =
    SimpleAttributeSet().apply {
        addAttribute(StyleConstants.CharacterConstants.Bold, bold)
        addAttribute(StyleConstants.CharacterConstants.Italic, italic)
        addAttribute(StyleConstants.CharacterConstants.Foreground, foreground)
    }

private val LOG_STYLES = mapOf(
    Level.ERROR to createStyle(bold = true, foreground = Color(153, 0, 0)),
    Level.WARN to createStyle(foreground = Color(153, 76, 0)),
    Level.INFO to createStyle(foreground = Color(0, 0, 153)),
    Level.DEBUG to createStyle(italic = true, foreground = Color(64, 64, 64)),
    Level.TRACE to createStyle(italic = true, foreground = Color(153, 0, 76))
)
private val LOG_STYLE_DEFAULT = createStyle(italic = true)

/**
 * Based on Appender by Rodrigo Garcia Lima (https://stackoverflow.com/a/33657637)
 */
class SwingLogger(private val lineLimit: Int = 2000) : AppenderBase<ILoggingEvent>() {
    override fun start() {
        patternLayout.context = context
        patternLayout.start()
        super.start()
    }

    override fun append(event: ILoggingEvent) {
        SwingUtilities.invokeLater {
            textPane.document.apply {
                val lineCountOverLimit = defaultRootElement.elementCount - lineLimit
                if (lineCountOverLimit > 0) {
                    remove(0, defaultRootElement.getElement(lineCountOverLimit).endOffset)
                }
            }
            textPane.document.insertString(
                textPane.document.length,
                patternLayout.doLayout(event),
                LOG_STYLES.getOrDefault(event.level, LOG_STYLE_DEFAULT)
            )
            textPane.caretPosition = textPane.document.length
        }
    }

    companion object {
        val textPane = JTextPane().apply { isEditable = false }
        private val patternLayout: PatternLayout = PatternLayout().apply {
            pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
        }
    }
}

To use it simply append the JTextPane somewhere by using the static property: add(JScrollPane(SwingLogger.textPane))

Also of course you still need to hook up the appender with logback, e.g. in logback.xml.

yankee
  • 38,872
  • 15
  • 103
  • 162