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.
This commit is contained in:
Markil3
2021-09-06 11:37:59 -06:00
parent 275873155c
commit 91980aac75
6 changed files with 413 additions and 3 deletions

2
.gitignore vendored
View File

@@ -7,3 +7,5 @@
/local.properties /local.properties
/profile/ /profile/
/firefox/ /firefox/
/*/*.log
*.log

View File

@@ -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<LogEvent> events = new LinkedList<>();
/**
* Builds FileAppender instances.
*
* @param <B> The type to build
*/
public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
implements org.apache.logging.log4j.core.util.Builder<ListAppender>
{
@Override
public ListAppender build()
{
return new ListAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray());
}
}
@PluginBuilderFactory
public static <B extends Builder<B>> B newBuilder() {
return new Builder<B>().asBuilder();
}
public ListAppender(final String name, final Filter filter, final Layout<? extends Serializable> 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<LogEvent> getLogEvents()
{
return events.stream().collect(Collectors.toUnmodifiableList());
}
}

View File

@@ -323,10 +323,14 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
{ {
if (this.isEnabled()) 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")) this.actions.put("about", action = new AbstractAction(langs.getString("actions.about"))
{ {

View File

@@ -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<String, JComponent> 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<String> 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);
}
}

View File

@@ -1,5 +1,14 @@
title=Universal Music Player 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.file.title=File
menu.view.title=View menu.view.title=View
menu.view.collections=Collections menu.view.collections=Collections

View File

@@ -2,7 +2,7 @@
<!-- <!--
~ Copyright (c) 2021 William Hubbard. All Rights Reserved. ~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
--> -->
<Configuration status="WARN"> <Configuration packages="edu.regis.universeplayer" status="WARN">
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_OUT"> <Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%c:%L %-5level - %msg%n"/> <PatternLayout pattern="%c:%L %-5level - %msg%n"/>
@@ -12,10 +12,17 @@
<Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern> <Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern>
</PatternLayout> </PatternLayout>
</File> </File>
<List name="List"/>
<Async name="Async">
<AppenderRef ref="File"/>
<AppenderRef ref="Console"/>
<AppenderRef ref="List"/>
</Async>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="debug"> <Root level="debug">
<AppenderRef ref="File"/> <AppenderRef ref="Async"/>
</Root> </Root>
<Logger name="BrowserRunner" level="info"/>
</Loggers> </Loggers>
</Configuration> </Configuration>