From: Tuukka Lehtonen Date: Thu, 21 Jan 2021 22:05:06 +0000 (+0200) Subject: Add UI dialog for dynamic reconfiguration of Logback loggers X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=commitdiff_plain;h=7dffec3cc59c7ffd73e47e5ee2d9c1e21b1a0537;hp=831925ed328433a88bb2d36a122b098b97af9a58;p=simantics%2Fplatform.git Add UI dialog for dynamic reconfiguration of Logback loggers gitlab #670 Change-Id: Icf4af49e7320108938075c27388b0616f5dd4a14 --- diff --git a/bundles/org.simantics.logging.ui/META-INF/MANIFEST.MF b/bundles/org.simantics.logging.ui/META-INF/MANIFEST.MF index 600285018..f2617b78d 100644 --- a/bundles/org.simantics.logging.ui/META-INF/MANIFEST.MF +++ b/bundles/org.simantics.logging.ui/META-INF/MANIFEST.MF @@ -10,6 +10,7 @@ Require-Bundle: javax.inject, org.eclipse.e4.ui.services, org.eclipse.e4.core.di.annotations, org.eclipse.core.runtime, + org.eclipse.ui, org.slf4j.api, org.simantics.logging, org.simantics.utils.ui diff --git a/bundles/org.simantics.logging.ui/fragment.e4xmi b/bundles/org.simantics.logging.ui/fragment.e4xmi index cc14bb5f8..fa7deef3c 100644 --- a/bundles/org.simantics.logging.ui/fragment.e4xmi +++ b/bundles/org.simantics.logging.ui/fragment.e4xmi @@ -5,10 +5,12 @@ + + @@ -31,6 +33,7 @@ + diff --git a/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/Activator.java b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/Activator.java index 942a88ba2..34bc2a659 100644 --- a/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/Activator.java +++ b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/Activator.java @@ -1,30 +1,50 @@ package org.simantics.logging.ui; -import org.osgi.framework.BundleActivator; +import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; -public class Activator implements BundleActivator { +/** + * The activator class controls the plug-in life cycle + */ +public class Activator extends AbstractUIPlugin { - private static BundleContext context; + // The plug-in ID + public static final String PLUGIN_ID = "org.simantics.logging.ui"; //$NON-NLS-1$ - static BundleContext getContext() { - return context; + // The shared instance + private static Activator plugin; + + /** + * The constructor + */ + public Activator() { } /* * (non-Javadoc) - * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext) + * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext) */ - public void start(BundleContext bundleContext) throws Exception { - Activator.context = bundleContext; + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; } /* * (non-Javadoc) - * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext) + * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext) + */ + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance */ - public void stop(BundleContext bundleContext) throws Exception { - Activator.context = null; + public static Activator getDefault() { + return plugin; } } diff --git a/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/LoggerManagementDialog.java b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/LoggerManagementDialog.java new file mode 100644 index 000000000..7ef5cc6c9 --- /dev/null +++ b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/LoggerManagementDialog.java @@ -0,0 +1,349 @@ +package org.simantics.logging.ui; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.IDialogSettings; +import org.eclipse.jface.dialogs.TrayDialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.layout.TableColumnLayout; +import org.eclipse.jface.resource.FontDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.ComboBoxCellEditor; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; +import org.simantics.logging.LogConfigurator; +import org.simantics.logging.LoggerLevel; + +public class LoggerManagementDialog extends TrayDialog { + + private static final String DIALOG = LoggerManagementDialog.class.getSimpleName(); //$NON-NLS-1$ + + private static final String[] LEVELS = { + "ERROR", + "WARN", + "INFO", + "DEBUG", + "TRACE", + }; + + private static final Map LEVEL_TO_INDEX = new HashMap<>(); + + static { + for (int i = 0; i < LEVELS.length; i++) { + LEVEL_TO_INDEX.put(LEVELS[i], i); + } + } + + /** + * ID for Apply buttons. + */ + private int APPLY_ID = IDialogConstants.CLIENT_ID + 1; + + /** + * The label for Apply buttons. + */ + private String APPLY_LABEL = JFaceResources.getString("apply"); //$NON-NLS-1$ + + private TableViewer tableViewer; + private List configuration; + private IDialogSettings dialogBoundsSettings; + + private Consumer> applyFunction; + + public LoggerManagementDialog(Shell shell, Consumer> applyFunction) { + super(shell); + reloadConfiguration(); + this.applyFunction = applyFunction; + + IDialogSettings settings = Activator.getDefault().getDialogSettings(); + dialogBoundsSettings = settings.getSection(DIALOG); + if (dialogBoundsSettings == null) + dialogBoundsSettings = settings.addNewSection(DIALOG); + } + + private void reloadConfiguration() { + this.configuration = LogConfigurator.listConfiguredLoggers(); + addEmptyRow(); + } + + private void addEmptyRow() { + this.configuration.add(new LoggerLevel("", LEVELS[2])); + } + + @Override + protected IDialogSettings getDialogBoundsSettings() { + return dialogBoundsSettings; + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Manage Loggers"); + newShell.setMinimumSize(800, 600); + } + + @Override + protected boolean isResizable() { + return true; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, APPLY_ID, APPLY_LABEL, false); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.CLOSE_ID) { + super.buttonPressed(IDialogConstants.CANCEL_ID); + } else if (buttonId == APPLY_ID) { + if (applyFunction != null) { + applyFunction.accept(configuration); + reloadConfiguration(); + setInput(); + } + } else { + super.buttonPressed(buttonId); + } + } + + @Override + protected Control createDialogArea(Composite parent) { + final Composite composite = (Composite) super.createDialogArea(parent); + GridLayoutFactory.fillDefaults().margins(10, 10).numColumns(1).applyTo(composite); + GridDataFactory.fillDefaults().grab(true, true).applyTo(composite); + + Composite tableComposite = new Composite(composite, SWT.NONE); + TableColumnLayout tcl = new TableColumnLayout(); + tableComposite.setLayout(tcl); + GridDataFactory.fillDefaults().grab(true, true).applyTo(tableComposite); + + tableViewer = new TableViewer(tableComposite, SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI); + + Display display = getShell().getDisplay(); + + // Fonts + final Font systemFont = display.getSystemFont(); + final Font italic = FontDescriptor.createFrom(systemFont).setStyle(SWT.ITALIC).createFont(display); + final Font bold = FontDescriptor.createFrom(systemFont).setStyle(SWT.BOLD).createFont(display); + tableViewer.getTable().addDisposeListener(e -> { + italic.dispose(); + bold.dispose(); + }); + + Function fontFunction = l -> { + if (l.isLoggerDefined()) { + if (l.levelChanged()) { + return bold; + } + return systemFont; + } + return italic; + }; + + // Column 1: logger name + TableViewerColumn column1 = new TableViewerColumn(tableViewer, SWT.NONE); + column1.getColumn().setWidth(300); + column1.getColumn().setText("Logger"); + column1.getColumn().setToolTipText("Package Name of Logger"); + column1.setLabelProvider(new ColumnLabelProvider() { + @Override + public void update(ViewerCell cell) { + LoggerLevel l = (LoggerLevel) cell.getElement(); + cell.setText(l.getName()); + cell.setFont(fontFunction.apply(l)); + } + }); + column1.setEditingSupport(new EditingSupport(tableViewer) { + CellEditor editor = new TextCellEditor(tableViewer.getTable()); + @Override + protected CellEditor getCellEditor(Object element) { + return editor; + } + @Override + protected boolean canEdit(Object element) { + LoggerLevel l = (LoggerLevel) element; + return !l.isLoggerDefined(); + } + @Override + protected Object getValue(Object element) { + return ((LoggerLevel)element).getName(); + } + @Override + protected void setValue(Object element, Object value) { + LoggerLevel l = (LoggerLevel) element; + String s = ((String) value).trim(); + + boolean sameNameExists = !s.isEmpty() && configuration.stream().anyMatch(ll -> ll != l && ll.getName().equals(s)); + + // Prevent providing the same package twice + if (sameNameExists) + return; + + String previousName = l.getName(); + l.setName(s); + if (s.isEmpty()) { + int index = configuration.indexOf(l); + if (index >= 0 && index < (configuration.size()-1)) { + configuration.remove(index); + } + } else if (previousName.isEmpty()) { + addEmptyRow(); + } + setInput(); + } + }); + + // Column 2: logging level of logger + TableViewerColumn column2 = new TableViewerColumn(tableViewer, SWT.NONE); + column2.getColumn().setWidth(100); + column2.getColumn().setText("Log Level"); + column2.getColumn().setToolTipText("Logging Level for Package and Subpackages"); + column2.setLabelProvider(new ColumnLabelProvider() { + @Override + public void update(ViewerCell cell) { + LoggerLevel l = (LoggerLevel) cell.getElement(); + cell.setText(l.getLevel()); + cell.setFont(fontFunction.apply(l)); + } + }); + column2.setEditingSupport(new EditingSupport(tableViewer) { + @Override + protected boolean canEdit(Object element) { + return true; + } + @Override + protected CellEditor getCellEditor(Object element) { + return new ComboBoxCellEditor(tableViewer.getTable(), LEVELS, SWT.READ_ONLY) { + @Override + protected Control createControl(Composite parent) { + CCombo combo = (CCombo) super.createControl(parent); + // The only way found to actually open the combo box list + // right away when starting to edit. + combo.getDisplay().asyncExec(() -> { + if (!combo.isDisposed()) + combo.setListVisible(true); + }); + return combo; + } + }; + } + @Override + protected Object getValue(Object element) { + LoggerLevel l = (LoggerLevel) element; + return LEVEL_TO_INDEX.get(l.getLevel()); + } + @Override + protected void setValue(Object element, Object value) { + LoggerLevel l = (LoggerLevel)element; + l.setLevel(LEVELS[(Integer) value]); + getViewer().update(element, null); + } + }); + + tcl.setColumnData(column1.getColumn(), new ColumnWeightData(5, 300)); + tcl.setColumnData(column2.getColumn(), new ColumnWeightData(1, 150)); + + // Decorations + tableViewer.getTable().setHeaderVisible(true); + tableViewer.getTable().setLinesVisible(true); + + // Table content + tableViewer.setContentProvider(ArrayContentProvider.getInstance()); + + tableViewer.getTable().addListener(SWT.KeyDown, e -> { + if (e.keyCode == SWT.DEL) { + @SuppressWarnings("unchecked") + List s = tableViewer.getStructuredSelection().toList(); + Predicate removable = l -> { + LoggerLevel ll = (LoggerLevel) l; + return ll.isLoggerDefined() && !ll.getName().isEmpty(); + }; + if (s.stream().allMatch(removable)) { + s.forEach(configuration::remove); + setInput(); + } + } + }); + + Text filterText = new Text(composite, SWT.FLAT | SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo(filterText); + filterText.setToolTipText("Package Name Filter"); + filterText.moveAbove(tableComposite); + filterText.addModifyListener(e -> { + String filter = filterText.getText().trim().toLowerCase(); + ViewerFilter[] filters = {}; + if (!filter.isEmpty()) { + filters = new ViewerFilter[] { + new ViewerFilter() { + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + LoggerLevel l = (LoggerLevel) element; + return !l.isLoggerDefined() || l.levelChanged() + ? true + : l.getName().toLowerCase().contains(filter); + } + } + }; + }; + scheduleSetFilters(filters); + }); + + setInput(); + + return composite; + } + + private void setInput() { + tableViewer.setInput(configuration.toArray(new LoggerLevel[configuration.size()])); + } + + public List getConfiguration() { + return configuration; + } + + private final AtomicReference filtersToSet = new AtomicReference<>(); + private final Runnable setFilters = () -> { + if (!tableViewer.getTable().isDisposed()) { + ViewerFilter[] fs = filtersToSet.getAndSet(null); + if (fs != null) { + tableViewer.getTable().setRedraw(false); + tableViewer.setFilters(fs); + tableViewer.getTable().setRedraw(true); + } + } + }; + + protected void scheduleSetFilters(ViewerFilter[] array) { + filtersToSet.set(array); + getShell().getDisplay().timerExec(250, setFilters); + } + +} diff --git a/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/handlers/ConfigureLoggingHandler.java b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/handlers/ConfigureLoggingHandler.java new file mode 100644 index 000000000..8518a4392 --- /dev/null +++ b/bundles/org.simantics.logging.ui/src/org/simantics/logging/ui/handlers/ConfigureLoggingHandler.java @@ -0,0 +1,33 @@ + +package org.simantics.logging.ui.handlers; + +import java.util.List; +import java.util.function.Consumer; + +import javax.inject.Named; + +import org.eclipse.e4.core.di.annotations.Execute; +import org.eclipse.e4.ui.services.IServiceConstants; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Shell; +import org.simantics.logging.LogConfigurator; +import org.simantics.logging.LoggerLevel; +import org.simantics.logging.ui.LoggerManagementDialog; + +/** + * @author Tuukka Lehtonen + * @since 1.47.0 + */ +public class ConfigureLoggingHandler { + + @Execute + public void execute(@Named(IServiceConstants.ACTIVE_SHELL) Shell shell) { + Consumer> applyConfiguration = LogConfigurator::applyLogLevels; + + LoggerManagementDialog dialog = new LoggerManagementDialog(shell, applyConfiguration); + if (dialog.open() == Window.OK) { + applyConfiguration.accept(dialog.getConfiguration()); + } + } + +} \ No newline at end of file diff --git a/bundles/org.simantics.logging/src/org/simantics/logging/LogConfigurator.java b/bundles/org.simantics.logging/src/org/simantics/logging/LogConfigurator.java index 047bac8b6..2928de681 100644 --- a/bundles/org.simantics.logging/src/org/simantics/logging/LogConfigurator.java +++ b/bundles/org.simantics.logging/src/org/simantics/logging/LogConfigurator.java @@ -1,6 +1,8 @@ package org.simantics.logging; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,13 +44,53 @@ public final class LogConfigurator { LOGGER.info("Setting logger level to {} for loggers {}", level, logger); LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); Level ll = getLoggerLevel(level); - ch.qos.logback.classic.Logger loggerList = context.getLogger(logger); - loggerList.setLevel(ll); + ch.qos.logback.classic.Logger l = context.getLogger(logger); + l.setLevel(ll); if (LOGGER.isDebugEnabled()) - LOGGER.debug("Loggers installed {}", loggerList); + LOGGER.debug("Logger {} level set to {}", l, ll); } private static Level getLoggerLevel(String level) { return Level.valueOf(level); } + + /** + * @since 1.47.0 + */ + public static List listConfiguredLoggers() { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + return context.getLoggerList().stream() + .map(l -> { + //LOGGER.info("{} : {} : {} : {}", l, l.getName(), l.getLevel(), l.getEffectiveLevel()); + if (l.getName().isEmpty()) + return null; + return new LoggerLevel(l, l.getEffectiveLevel().toString()); + }) + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()); + } + + /** + * @since 1.47.0 + */ + public static void applyLogLevels(List loggers) { + loggers.forEach(l -> { + Level level = getLoggerLevel(l.getLevel()); + Logger logger = l.getLogger(); + if (logger instanceof ch.qos.logback.classic.Logger) { + LOGGER.info("Setting existing logger {} level to {}", l.getName(), l.getLevel()); + ch.qos.logback.classic.Logger ll = (ch.qos.logback.classic.Logger) l.getLogger(); + ll.setLevel(level); + } else if (!l.getName().trim().isEmpty()) { + LOGGER.info("Defining new logger {} with level {}", l.getName(), l.getLevel()); + logger = LoggerFactory.getLogger(l.getName()); + if (logger instanceof ch.qos.logback.classic.Logger) { + ch.qos.logback.classic.Logger ll = (ch.qos.logback.classic.Logger) logger; + ll.setLevel(level); + } + } + }); + } + } diff --git a/bundles/org.simantics.logging/src/org/simantics/logging/LoggerLevel.java b/bundles/org.simantics.logging/src/org/simantics/logging/LoggerLevel.java new file mode 100644 index 000000000..0beeb26c0 --- /dev/null +++ b/bundles/org.simantics.logging/src/org/simantics/logging/LoggerLevel.java @@ -0,0 +1,94 @@ +package org.simantics.logging; + +import java.util.Objects; + +import org.slf4j.Logger; + +/** + * @author Tuukka Lehtonen + * @since 1.47.0 + */ +public class LoggerLevel implements Comparable { + + private final Logger logger; + private final String originalLevel; + private String loggerName; + private String level; + + public LoggerLevel(Logger logger, String level) { + Objects.requireNonNull(logger, "logger"); + Objects.requireNonNull(level, "level"); + this.logger = logger; + this.originalLevel = level; + this.loggerName = logger.getName(); + this.level = level; + } + + public LoggerLevel(String loggerName, String level) { + Objects.requireNonNull(loggerName, "loggerName"); + Objects.requireNonNull(level, "level"); + this.logger = null; + this.originalLevel = null; + this.loggerName = loggerName; + this.level = level; + } + + Logger getLogger() { + return logger; + } + + public String getName() { + return loggerName; + } + + public LoggerLevel setName(String name) { + if (logger != null) + throw new UnsupportedOperationException("Cannot rename already created logger " + loggerName + " to " + name); + Objects.requireNonNull(name, "name"); + this.loggerName = name; + return this; + } + + public String getLevel() { + return level; + } + + public LoggerLevel setLevel(String level) { + Objects.requireNonNull(level, "level"); + this.level = level; + return this; + } + + public boolean levelChanged() { + return !Objects.equals(originalLevel, level); + } + + public boolean isLoggerDefined() { + return logger != null; + } + + @Override + public int hashCode() { + return Objects.hash(level, logger, loggerName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + LoggerLevel other = (LoggerLevel) obj; + return Objects.equals(level, other.level) + && Objects.equals(logger, other.logger) + && Objects.equals(loggerName, other.loggerName); + } + + @Override + public int compareTo(LoggerLevel o) { + return loggerName.compareToIgnoreCase(o.loggerName); + } + +}