/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2010 A.Brochard
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.logging;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.ThresholdFilter;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.net.SyslogAppender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.OutputStreamAppender;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.pms.PMS;
import net.pms.configuration.PmsConfiguration;
import net.pms.util.Iterators;
import net.pms.util.PropertiesUtil;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
/**
* Simple loader for logback configuration files.
*
* @author thomas@innot.de, expanded by Nadahar
*/
public class LoggingConfig {
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(LoggingConfig.class);
private static Object filepathLock = new Object();
private static String filepath = null;
private static Object logFilePathsLock = new Object();
private static HashMap<String, String> logFilePaths = new HashMap<>(); // key: appender name, value: log file path
private static LoggerContext loggerContext = null;
private static Logger rootLogger;
private static SyslogAppender syslog;
private static boolean syslogDisabled = false;
private static enum ActionType { START, STOP, NONE };
private static Level consoleLevel = null;
private static Level tracesLevel = null;
private static LinkedList<Appender<ILoggingEvent>> syslogDetachedAppenders = new LinkedList<>();
/** Not to be instantiated. */
private LoggingConfig() {
}
/**
* Gets the full path of a successfully loaded Logback configuration file.
*
* If the configuration file could not be loaded the string
* <code>internal defaults</code> is returned.
*
* @return pathname or <code>null</code>
*/
public static String getConfigFilePath() {
synchronized (filepathLock) {
if (filepath != null) {
return filepath;
}
return "internal defaults";
}
}
private static File getFile(String[] fileList) {
for (String fileName : fileList) {
fileName = fileName.trim();
if (fileName.length() > 0) {
if (fileName.matches("\\[PROFILE_DIR\\].*")) {
String s = PMS.getConfiguration().getProfileDirectory().replace("\\", "/");
fileName = fileName.replaceAll("\\[PROFILE_DIR\\]", s);
}
File file = new File(fileName.trim());
if (file.exists() && file.canRead()) {
return file;
}
}
}
return null;
}
private static boolean setContextAndRoot() {
ILoggerFactory ilf = LoggerFactory.getILoggerFactory();
if (!(ilf instanceof LoggerContext)) {
// Not using LogBack.
// Can't configure the logger, so just exit
LOGGER.debug("Not using LogBack, aborting LogBack configuration");
return false;
}
loggerContext = (LoggerContext) ilf;
rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
if (rootLogger == null) {
// Shouldn't be possible
LOGGER.error("Couldn't find root logger, aborting LogBack configuration");
return false;
}
return true;
}
/**
* Loads the (optional) Logback configuration file.
*
* It loads the file defined in the <code>project.logback</code> property from the current
* directory and (re-)initializes Logback with this file. If running
* headless (<code>System.Property("console")</code> set), then the
* alternative config file defined in <code>project.logback.headless</code> is tried first.
*
* If no config file can be found in the CWD, then nothing is loaded and
* Logback will use the logback.xml file on the classpath as a default. If
* this doesn't exist then a basic console appender is used as fallback.
*
* <strong>Note:</strong> Any error messages generated while parsing the
* config file are dumped only to <code>stdout</code>.
*/
public static synchronized void loadFile() {
File file = null;
if (!setContextAndRoot()) {
return;
}
if (PMS.isHeadless()) {
file = getFile(PropertiesUtil.getProjectProperties().get("project.logback.headless").split(","));
}
if (file == null) {
file = getFile(PropertiesUtil.getProjectProperties().get("project.logback").split(","));
}
if (file == null) {
// Unpredictable: Any logback.xml found in the Classpath is loaded, if that fails defaulting to BasicConfigurator
// See http://logback.qos.ch/xref/ch/qos/logback/classic/BasicConfigurator.html
LOGGER.warn("Could not load LogBack configuration file from " + (PMS.isHeadless() ?
PropertiesUtil.getProjectProperties().get("project.logback.headless") + ", " : "") +
PropertiesUtil.getProjectProperties().get("project.logback"));
LOGGER.warn("Falling back to somewhat unpredictable defaults, probably only logging to console.");
return;
}
// Now get logback to actually use the config file
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(loggerContext);
try {
// the context was probably already configured by
// default configuration rules
loggerContext.reset();
loggerContext.getStatusManager().clear();
// Do not log between loggerContext.reset() and CacheLogger.initContext()
configurator.doConfigure(file);
if (CacheLogger.isActive()) {
CacheLogger.initContext();
}
// Save the file path after loading the file
synchronized (filepathLock) {
filepath = file.getAbsolutePath();
LOGGER.debug("LogBack started with configuration file: {}", filepath);
}
} catch (JoranException je) {
try {
System.err.println("LogBack configuration failed: " + je.getLocalizedMessage());
System.err.println("Trying to create \"emergency\" configuration");
// Try to create "emergency" appenders for some logging if configuration fails
if (PMS.isHeadless()) {
ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<>();
PatternLayoutEncoder pe = new PatternLayoutEncoder();
pe.setPattern("%-5level %d{HH:mm:ss.SSS} [%thread] %logger %msg%n");
pe.setContext(loggerContext);
pe.start();
ca.setEncoder(pe);
ca.setContext(loggerContext);
ca.setName("Emergency Console");
ca.start();
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(ca);
} else {
FrameAppender<ILoggingEvent> fa = new FrameAppender<>();
PatternLayoutEncoder pe = new PatternLayoutEncoder();
pe.setPattern("%-5level %d{HH:mm:ss.SSS} [%thread] %logger %msg%n");
pe.setContext(loggerContext);
pe.start();
fa.setEncoder(pe);
fa.setContext(loggerContext);
fa.setName("Emergency Frame");
fa.start();
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(fa);
}
System.err.println("LogBack \"emergency\" configuration applied.");
} catch (Exception e) {
System.err.println("LogBack \"emergency\" configuration failed with: " + e);
}
if (CacheLogger.isActive()) {
CacheLogger.initContext();
}
LOGGER.error("Logback configuration failed with: {}", je.getLocalizedMessage());
StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
return;
}
// Build the iterator
Iterators<Appender<ILoggingEvent>> iterators = new Iterators<>();
// Add CacheLogger appenders if CacheLogger is active
if (CacheLogger.isActive()) {
iterators.addIterator(CacheLogger.iteratorForAppenders());
}
// syslogDetachedAppenders can't be populated at this stage, so no reason to iterate it.
// Iterate loggerContext even if CacheLogger is active as there could still be
// non-root appenders there.
for (Logger logger : loggerContext.getLoggerList()) {
iterators.addIterator(logger.iteratorForAppenders());
}
// Iterate
Iterator<Appender<ILoggingEvent>> it = iterators.combinedIterator();
synchronized (logFilePathsLock) {
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (appender instanceof FileAppender) {
FileAppender<ILoggingEvent> fa = (FileAppender<ILoggingEvent>) appender;
logFilePaths.put(fa.getName(), fa.getFile());
} else if (appender instanceof SyslogAppender) {
syslogDisabled = true;
}
}
}
// Set filters for console and traces
setConfigurableFilters(true, true);
StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
return;
}
private static synchronized void setConfigurableFilters(boolean setConsole, boolean setTraces) {
PmsConfiguration configuration = PMS.getConfiguration();
if (loggerContext == null) {
LOGGER.error("Unknown loggerContext, aborting buffered logging. Make sure that loadFile() has been called first.");
return;
}
if (setConsole) {
Level level = configuration.getLoggingFilterConsole();
if (consoleLevel == null || consoleLevel != level) {
consoleLevel = level;
} else {
setConsole = false;
}
}
if (setTraces) {
Level level = configuration.getLoggingFilterLogsTab();
if (tracesLevel == null || tracesLevel != level) {
tracesLevel = level;
} else {
setTraces = false;
}
}
if (setConsole || setTraces) {
// Since Console- and FrameAppender will exist at root level and won't be detached by syslog,
// there's no reason to build an iterator as this should suffice.
Iterator<Appender<ILoggingEvent>> it =
CacheLogger.isActive() ?
CacheLogger.iteratorForAppenders() :
rootLogger.iteratorForAppenders();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (setConsole && appender instanceof ConsoleAppender) {
ConsoleAppender<ILoggingEvent> ca = (ConsoleAppender<ILoggingEvent>) appender;
boolean createNew = true;
if (!ca.getCopyOfAttachedFiltersList().isEmpty()) {
for (Filter<ILoggingEvent> filter : ca.getCopyOfAttachedFiltersList()) {
if (filter instanceof ThresholdFilter) {
createNew = false;
((ThresholdFilter) filter).setLevel(consoleLevel.levelStr);
((ThresholdFilter) filter).start();
}
}
}
if (createNew) {
ThresholdFilter consoleFilter = new ThresholdFilter();
ca.addFilter(consoleFilter);
consoleFilter.setLevel(consoleLevel.levelStr);
consoleFilter.setContext(loggerContext);
consoleFilter.start();
}
}
if (setTraces && appender instanceof FrameAppender) {
FrameAppender<ILoggingEvent> fa = (FrameAppender<ILoggingEvent>) appender;
boolean createNew = true;
if (!fa.getCopyOfAttachedFiltersList().isEmpty()) {
for (Filter<ILoggingEvent> filter : fa.getCopyOfAttachedFiltersList()) {
if (filter instanceof ThresholdFilter) {
createNew = false;
((ThresholdFilter) filter).setLevel(tracesLevel.levelStr);
((ThresholdFilter) filter).start();
}
}
}
if (createNew) {
ThresholdFilter tracesFilter = new ThresholdFilter();
fa.addFilter(tracesFilter);
tracesFilter.setLevel(tracesLevel.levelStr);
tracesFilter.setContext(loggerContext);
tracesFilter.start();
}
}
}
}
}
/**
* Finds and configures the console appender with a filter with
* the logging level from configuration provided that it's not already
* configured in logback.xml or logback.headless.xml.
*/
public static void setConsoleFilter() {
setConfigurableFilters(true, false);
}
/**
* Finds and configures the frame appender with a filter with
* the logging level from configuration provided that it's not already
* configured in logback.xml or logback.headless.xml.
*/
public static void setTracesFilter() {
setConfigurableFilters(false, true);
}
/**
* Shows whether or not UMS' automated syslog function is disabled because
* one or more syslog appenders are manually configured in LogBack
* configuration.
* @return the status.
*/
public static synchronized boolean isSyslogDisabled() {
return syslogDisabled;
}
/**
* Adds/modifies/removes a syslog appender based on PmsConfiguration and
* disables/enables file appenders for easier access to syslog logging for
* users without in-depth knowledge of LogBack. Stops file appenders if
* syslog is started and vice versa.<P>
*
* Must be called after {@link #loadFile()} and after UMS configuration is
* loaded.
*/
public static synchronized void setSyslog() {
ActionType action = ActionType.NONE;
PmsConfiguration configuration = PMS.getConfiguration();
if (loggerContext == null) {
LOGGER.error("Unknown loggerContext, aborting syslog configuration. Make sure that loadFile() has been called first.");
return;
} else if (syslogDisabled) {
//Only create a new syslog appender if there's no syslog appender configured already
LOGGER.warn("A syslog appender is already configured, aborting syslog configuration");
return;
}
if (configuration.getLoggingUseSyslog()) {
// Check for valid parameters
if (configuration.getLoggingSyslogHost().isEmpty()) {
LOGGER.error("Empty syslog hostname, syslog configuration aborted");
return;
}
try {
InetAddress.getByName(configuration.getLoggingSyslogHost());
} catch (UnknownHostException e) {
LOGGER.error("Unknown syslog hostname {}, syslog configuration aborted", configuration.getLoggingSyslogHost());
return;
}
if (configuration.getLoggingSyslogPort() < 1 && configuration.getLoggingSyslogPort() > 65535) {
LOGGER.error("Invalid syslog port {}, using default", configuration.getLoggingSyslogPort());
configuration.setLoggingSyslogPortDefault();
}
if (!configuration.getLoggingSyslogFacility().toLowerCase().matches(
"auth|authpriv|daemon|cron|ftp|lpr|kern|mail|news|syslog|user|" +
"uucp|local0|local1|local2|local3|local4|local5|local6|local7")
) {
LOGGER.error("Invalid syslog facility \"{}\", using default", configuration.getLoggingSyslogFacility());
configuration.setLoggingSyslogFacilityDefault();
}
}
if (configuration.getLoggingUseSyslog() && syslog == null) {
syslog = new SyslogAppender();
syslog.setContext(loggerContext);
syslog.setSuffixPattern("UMS [%thread] %msg");
syslog.setName("UMS syslog");
syslog.setCharset(StandardCharsets.UTF_8);
action = ActionType.START;
} else if (!configuration.getLoggingUseSyslog() && syslog != null) {
action = ActionType.STOP;
}
if (syslog != null && (action == ActionType.START || action == ActionType.NONE)) {
syslog.setSyslogHost(configuration.getLoggingSyslogHost());
syslog.setPort(configuration.getLoggingSyslogPort());
syslog.setFacility(configuration.getLoggingSyslogFacility().toUpperCase());
syslog.start();
}
if (action == ActionType.START || action == ActionType.STOP) {
Iterator<Appender<ILoggingEvent>> it =
CacheLogger.isActive() ?
CacheLogger.iteratorForAppenders() :
rootLogger.iteratorForAppenders();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (action == ActionType.START && appender instanceof FileAppender) {
if (CacheLogger.isActive()) {
CacheLogger.removeAppender(appender);
} else {
rootLogger.detachAppender(appender);
}
syslogDetachedAppenders.add(appender);
// If syslog is disabled later and this appender reactivated, append to the file instead of truncate
((FileAppender<ILoggingEvent>) appender).setAppend(true);
} else if (action == ActionType.STOP && appender == syslog) {
if (CacheLogger.isActive()) {
CacheLogger.removeAppender(syslog);
} else {
rootLogger.detachAppender(syslog);
}
syslog.stop();
syslog = null;
}
}
if (action == ActionType.START) {
if (CacheLogger.isActive()) {
CacheLogger.addAppender(syslog);
} else {
rootLogger.addAppender(syslog);
}
LOGGER.info("Syslog logging started, file logging disabled");
} else {
it = syslogDetachedAppenders.iterator();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (CacheLogger.isActive()) {
CacheLogger.addAppender(appender);
} else {
rootLogger.addAppender(appender);
}
}
syslogDetachedAppenders.clear();
LOGGER.info("Syslog logging stopped, file logging enabled");
}
}
}
/**
* Turns ImmediateFlush off or on (!buffered) for all encoders descending from
* LayoutWrappingEncoder for appenders descending from OutputStreamAppender
* except ConsoleAppender. The purpose is to turn on or off buffering/flushing
* for all file logging appenders.
*
* Must be called after {@link #loadFile()}.
*
* @param buffered whether or not file logging should be buffered or flush immediately
*/
public static synchronized void setBuffered(boolean buffered) {
if (loggerContext == null) {
LOGGER.error("Unknown loggerContext, aborting buffered logging. Make sure that loadFile() has been called first.");
return;
}
// Build iterator
Iterators<Appender<ILoggingEvent>> iterators = new Iterators<>();
// Add CacheLogger or rootLogger appenders depending on whether CacheLogger is active.
if (CacheLogger.isActive()) {
iterators.addIterator(CacheLogger.iteratorForAppenders());
} else {
iterators.addIterator(rootLogger.iteratorForAppenders());
}
// If syslog is active there probably are detached appenders there as well
if (!syslogDetachedAppenders.isEmpty()) {
iterators.addList(syslogDetachedAppenders);
}
// Iterate
Iterator<Appender<ILoggingEvent>> it = iterators.combinedIterator();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (appender instanceof OutputStreamAppender && !(appender instanceof ConsoleAppender<?>)) {
// Appender has ImmediateFlush property
((OutputStreamAppender<ILoggingEvent>) appender).setImmediateFlush(!buffered);
}
}
LOGGER.info("Buffered logging turned {}", buffered ? "ON" : "OFF");
}
/**
* Sets the root logger level.
*
* Must be called after {@link #loadFile()}.
*
* @param level the new root logger level.
*/
public static synchronized void setRootLevel(Level level) {
if (loggerContext == null && !setContextAndRoot()) {
LOGGER.error("Unknown loggerContext, aborting setRootLevel");
return;
}
rootLogger.setLevel(level);
}
/**
* Gets the root logger level.
*
* Must be called after {@link #loadFile()}.
*
* @return the root logger {@link Level}.
*/
public static synchronized Level getRootLevel() {
if (loggerContext == null && !setContextAndRoot()) {
LOGGER.error("Unknown loggerContext, aborting getRootLevel.");
return Level.OFF;
}
return rootLogger.getLevel();
}
public static synchronized void forceVerboseFileEncoder() {
final String timeStampFormat = "yyyy-MM-dd HH:mm:ss.SSS";
if (loggerContext == null) {
LOGGER.error("Unknown loggerContext, aborting buffered logging. Make sure that loadFile() has been called first.");
return;
}
// Build iterator
Iterators<Appender<ILoggingEvent>> iterators = new Iterators<>();
// Add CacheLogger or rootLogger appenders depending on whether CacheLogger is active.
if (CacheLogger.isActive()) {
iterators.addIterator(CacheLogger.iteratorForAppenders());
} else {
iterators.addIterator(rootLogger.iteratorForAppenders());
}
// If syslog is active there probably are detached appenders there as well
if (!syslogDetachedAppenders.isEmpty()) {
iterators.addList(syslogDetachedAppenders);
}
// Iterate
Iterator<Appender<ILoggingEvent>> it = iterators.combinedIterator();
while (it.hasNext()) {
Appender<ILoggingEvent> appender = it.next();
if (appender instanceof OutputStreamAppender && !(appender instanceof ConsoleAppender<?>)) {
// Appender has Encoder property
Encoder<ILoggingEvent> encoder = ((OutputStreamAppender<ILoggingEvent>) appender).getEncoder();
if (encoder instanceof PatternLayoutEncoder) {
// Encoder has pattern
PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) encoder;
String logPattern = patternEncoder.getPattern();
// Set timestamp format
Pattern pattern = Pattern.compile("%((date|d)(\\{([^\\}]*)\\})?)(?=\\s)");
Matcher matcher = pattern.matcher(logPattern);
if (matcher.find()) {
boolean replace = true;
if (matcher.group(4) != null && matcher.group(4).equals(timeStampFormat)) {
replace = false;
}
if (replace) {
logPattern = logPattern.replaceFirst(pattern.pattern(), "%d{" + timeStampFormat + "}");
}
} else {
if (logPattern.startsWith("%-5level")) {
logPattern = logPattern.substring(0, 8) + " %d{" + timeStampFormat + "}" + logPattern.substring(8);
} else {
logPattern = "d%{" + timeStampFormat + "} " + logPattern;
}
}
// Make sure %logger is included
pattern = Pattern.compile("((%logger|%lo|%c)(\\{\\d+\\})?)(?=\\s)");
matcher = pattern.matcher(logPattern);
if (matcher.find()) {
boolean replace = true;
if (matcher.group().equals("%logger")) {
replace = false;
}
if (replace) {
logPattern = logPattern.replaceFirst(pattern.pattern(), "%logger");
}
} else {
if (logPattern.contains("%msg")) {
logPattern = logPattern.substring(
0,
logPattern.indexOf("%msg")) + "%logger " + logPattern.substring(logPattern.indexOf("%msg")
);
} else {
logPattern = "%logger " + logPattern;
}
}
// Activate changes
patternEncoder.setPattern(logPattern);
patternEncoder.start();
}
}
}
LOGGER.info("Verbose file logging pattern enforced");
}
public static HashMap<String, String> getLogFilePaths() {
synchronized (logFilePathsLock) {
return logFilePaths;
}
}
}