From 91980aac7509b54a9617e62fdd071c3d351ce0fb Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Mon, 6 Sep 2021 11:37:59 -0600 Subject: [PATCH] Adds a way to view interface logs from within the interface. Honestly, my tool (with filtration utilities to boot) is probably more useful than regular logs. --- .gitignore | 2 + .../regis/universeplayer/ListAppender.java | 75 +++++ .../universeplayer/player/Interface.java | 6 +- .../universeplayer/player/LogWindow.java | 313 ++++++++++++++++++ .../main/resources/lang/interface.properties | 9 + interface/src/main/resources/log4j2.xml | 11 +- 6 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 interface/src/main/java/edu/regis/universeplayer/ListAppender.java create mode 100644 interface/src/main/java/edu/regis/universeplayer/player/LogWindow.java diff --git a/.gitignore b/.gitignore index eb2194a..46c0917 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /local.properties /profile/ /firefox/ +/*/*.log +*.log \ No newline at end of file diff --git a/interface/src/main/java/edu/regis/universeplayer/ListAppender.java b/interface/src/main/java/edu/regis/universeplayer/ListAppender.java new file mode 100644 index 0000000..0a9e328 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/ListAppender.java @@ -0,0 +1,75 @@ +package edu.regis.universeplayer; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +@Plugin( + name = "List", + category = Core.CATEGORY_NAME, + elementType = Appender.ELEMENT_TYPE) +public class ListAppender extends AbstractAppender +{ + private static final LinkedList events = new LinkedList<>(); + + /** + * Builds FileAppender instances. + * + * @param The type to build + */ + public static class Builder> extends AbstractAppender.Builder + implements org.apache.logging.log4j.core.util.Builder + { + @Override + public ListAppender build() + { + return new ListAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray()); + } + } + + @PluginBuilderFactory + public static > B newBuilder() { + return new Builder().asBuilder(); + } + + public ListAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final Property[] properties) + { + super(name, filter, layout, ignoreExceptions, properties); + } + + /** + * Logs a LogEvent using whatever logic this Appender wishes to use. It is typically recommended to use a + * bridge pattern not only for the benefits from decoupling an Appender from its implementation, but it is also + * handy for sharing resources which may require some form of locking. + * + * @param event The LogEvent. + */ + @Override + public void append(LogEvent event) + { + events.add(event); + } + + public static synchronized List getLogEvents() + { + return events.stream().collect(Collectors.toUnmodifiableList()); + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java index b5e24e2..ab72826 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java @@ -323,10 +323,14 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL { if (this.isEnabled()) { - + LogWindow logs = new LogWindow(); + logs.pack(); + logs.setSize(600, 400); + logs.setVisible(true); } } }); + action.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F12, 0)); this.actions.put("about", action = new AbstractAction(langs.getString("actions.about")) { diff --git a/interface/src/main/java/edu/regis/universeplayer/player/LogWindow.java b/interface/src/main/java/edu/regis/universeplayer/player/LogWindow.java new file mode 100644 index 0000000..7b0ecd4 --- /dev/null +++ b/interface/src/main/java/edu/regis/universeplayer/player/LogWindow.java @@ -0,0 +1,313 @@ +package edu.regis.universeplayer.player; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Comparator; +import java.util.Formatter; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import edu.regis.universeplayer.ListAppender; + +public class LogWindow extends JFrame +{ + private static final Logger logger = LoggerFactory.getLogger(LogWindow.class); + private static final ResourceBundle langs = ResourceBundle + .getBundle("lang.interface", Locale.getDefault()); + + private final HashMap filters = new HashMap<>(); + private final Box filterBox; + private final Box logBox; + private final JScrollPane centerView; + private final JMenu loggers; + private int lastLogRecord = 0; + private Thread logUpdaterThread; + private boolean active = true; + + public LogWindow() + { + super(); + + this.setTitle(langs.getString("logs.title")); + + FlowLayout layout = new FlowLayout(); + JPanel header = new JPanel(); + header.setLayout(layout); + + this.filterBox = Box.createHorizontalBox(); + header.add(filterBox); + + JPopupMenu filterMenu = new JPopupMenu("Filters"); + JButton filterButton = new JButton("\u25BC"); + filterButton.addMouseListener(new MouseAdapter() + { + /** + * {@inheritDoc} + * + * @param e + */ + @Override + public void mouseClicked(MouseEvent e) + { + filterMenu.show(e.getComponent(), e.getX(), e.getY()); + } + }); + header.add(filterButton); + + JMenu levels = new JMenu(langs.getString("logs.levels")); + filterMenu.add(levels); + JMenuItem levelTrace = new JMenuItem(langs.getString("logs.levels.trace")); + levelTrace.addActionListener(e -> addFilter("levels.trace")); + levels.add(levelTrace); + JMenuItem levelDebug = new JMenuItem(langs.getString("logs.levels.debug")); + levelDebug.addActionListener(e -> addFilter("levels.debug")); + levels.add(levelDebug); + JMenuItem levelInfo = new JMenuItem(langs.getString("logs.levels.info")); + levelInfo.addActionListener(e -> addFilter("levels.info")); + levels.add(levelInfo); + JMenuItem levelWarning = new JMenuItem(langs.getString("logs.levels.warning")); + levelWarning.addActionListener(e -> addFilter("levels.warning")); + levels.add(levelWarning); + JMenuItem levelError = new JMenuItem(langs.getString("logs.levels.error")); + levelError.addActionListener(e -> addFilter("levels.error")); + levels.add(levelError); + + this.loggers = new JMenu(langs.getString("logs.loggers")); + filterMenu.add(this.loggers); + + this.getContentPane().add(header, BorderLayout.PAGE_START); + + this.logBox = Box.createVerticalBox(); + + this.centerView = new JScrollPane(this.logBox); + this.getContentPane().add(centerView, BorderLayout.CENTER); + + this.addWindowListener(new WindowAdapter() + { + /** + * Invoked when a window is in the process of being closed. + * The close operation can be overridden at this point. + * + * @param e + */ + @Override + public void windowClosing(WindowEvent e) + { + active = false; + } + }); + + this.logUpdaterThread = new Thread(() -> { + while (active) + { + if (ListAppender.getLogEvents().size() > this.lastLogRecord) + { + this.lastLogRecord = ListAppender.getLogEvents().size(); + SwingUtilities.invokeLater(() -> this.resetLogs()); + try + { + Thread.sleep(1000); + } + catch (InterruptedException e) + { + logger.error("Could not sleep", e); + } + } + } + /* + * Stop + */ + }); + this.logUpdaterThread.start(); + } + + private void addFilter(String filter) + { + if (!this.filters.containsKey(filter)) + { + Box filterBox = Box.createHorizontalBox(); + if (langs.containsKey("logs." + filter)) + { + filterBox.add(new JLabel(langs.getString("logs." + filter))); + } + else + { + filterBox.add(new JLabel(filter.substring(filter.indexOf('.') + 1))); + } + + filterBox.add(new JButton(new AbstractAction("X") + { + @Override + public void actionPerformed(ActionEvent e) + { + removeFilter(filter); + } + })); + this.filters.put(filter, filterBox); + this.filterBox.add(filterBox); + this.resetLogs(); + this.filterBox.revalidate(); + this.filterBox.repaint(); + } + } + + private void removeFilter(String filter) + { + JComponent component = this.filters.remove(filter); + if (component != null) + { + component.getParent().remove(component); + this.resetLogs(); + this.filterBox.revalidate(); + this.filterBox.repaint(); + } + } + + private synchronized void resetLogs() + { + JScrollBar scroll = this.centerView.getVerticalScrollBar(); + boolean bottom = scroll.getModel().getValue() >= scroll.getModel().getMaximum() - scroll + .getModel().getExtent(); + this.logBox.removeAll(); + this.loggers.removeAll(); + ListAppender.getLogEvents().stream().map(LogEvent::getLoggerName).distinct().forEach(name -> { + JMenuItem loggerItem = new JMenuItem(name); + loggerItem.addActionListener(e -> addFilter("loggers." + name)); + this.loggers.add(loggerItem); + }); + ListAppender.getLogEvents().stream().filter(event -> { + Set filters = this.filters.keySet(); + if (filters.stream().anyMatch(filter -> filter.startsWith("levels"))) + { + switch (event.getLevel().name()) + { + case "TRACE": + if (!filters.stream().anyMatch(f -> f.equals("levels.trace"))) + { + return false; + } + break; + case "DEBUG": + if (!filters.stream().anyMatch(f -> f.equals("levels.debug"))) + { + return false; + } + break; + case "WARN": + if (!filters.stream().anyMatch(f -> f.equals("levels.warning"))) + { + return false; + } + break; + case "ERROR": + case "FATAL": + if (!filters.stream().anyMatch(f -> f.equals("levels.error"))) + { + return false; + } + break; + default: + if (!filters.stream().anyMatch(f -> f.equals("levels.info"))) + { + return false; + } + break; + } + } + if (filters.stream().anyMatch(f -> f.startsWith("loggers"))) + { + String loggerName = "loggers." + event.getLoggerName(); + if (!filters.stream().anyMatch(f -> f.equals(loggerName))) + { + return false; + } + } + return true; + }).sorted(Comparator.comparingLong(LogEvent::getNanoTime)).forEach(event -> { + Formatter form = new Formatter(); + JLabel label = new JLabel(form + .format("%tT - %s - %s", event.getNanoTime(), event.getLoggerName(), event + .getMessage().getFormattedMessage()).toString()); + form = new Formatter(); + label.setToolTipText(form.format("(%s#%s:%d)", Optional + .ofNullable(event.getSource()).map(StackTraceElement::getClassName) + .orElse(""), Optional + .ofNullable(event.getSource()).map(StackTraceElement::getMethodName) + .orElse(""), Optional + .ofNullable(event.getSource()).map(StackTraceElement::getLineNumber).orElse(-1)) + .toString()); + switch (event.getLevel().name()) + { + case "TRACE": + label.setForeground(Color.GRAY); + break; + case "DEBUG": + label.setForeground(Color.BLUE); + break; + case "WARN": + label.setForeground(Color.ORANGE); + break; + case "ERROR": + case "FATAL": + label.setForeground(Color.RED); + break; + default: + label.setForeground(Color.BLACK); + break; + } + logBox.add(label); + }); + this.logBox.revalidate(); + this.logBox.repaint(); + if (bottom) + { + SwingUtilities.invokeLater(() -> { + scroll.setValue(scroll.getMaximum()); + }); + } + } + + public static void main(String[] args) + { + LogWindow window = new LogWindow(); + window.pack(); + window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.setVisible(true); + } +} diff --git a/interface/src/main/resources/lang/interface.properties b/interface/src/main/resources/lang/interface.properties index 0d77f71..f140e2b 100644 --- a/interface/src/main/resources/lang/interface.properties +++ b/interface/src/main/resources/lang/interface.properties @@ -1,5 +1,14 @@ title=Universal Music Player +logs.title=Logs +logs.levels=Levels +logs.levels.trace=Trace +logs.levels.debug=Debug +logs.levels.info=Info +logs.levels.warning=Warning +logs.levels.error=Error +logs.loggers=Loggers + menu.file.title=File menu.view.title=View menu.view.collections=Collections diff --git a/interface/src/main/resources/log4j2.xml b/interface/src/main/resources/log4j2.xml index f67fe99..7c63f02 100644 --- a/interface/src/main/resources/log4j2.xml +++ b/interface/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ - + @@ -12,10 +12,17 @@ %d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n + + + + + + - + + \ No newline at end of file