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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/profile/
|
/profile/
|
||||||
/firefox/
|
/firefox/
|
||||||
|
/*/*.log
|
||||||
|
*.log
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user