package org.apache.sling.extensions.logback.internal;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.gaffer.GafferUtil;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggerContextAwareBase;
import ch.qos.logback.classic.spi.LoggerContextListener;
import ch.qos.logback.classic.util.EnvUtil;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.joran.GenericConfigurator;
import ch.qos.logback.core.joran.event.SaxEvent;
import ch.qos.logback.core.joran.spi.InterpretationContext;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.status.OnConsoleStatusListener;
import ch.qos.logback.core.status.StatusListener;
import ch.qos.logback.core.status.StatusListenerAsList;
import ch.qos.logback.core.status.StatusUtil;
import ch.qos.logback.core.util.StatusPrinter;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.LoggerFactory;
public class LogbackManager extends LoggerContextAwareBase {
private static final String PREFIX = "org.apache.sling.commons.log";
private static final String DEBUG = PREFIX + "." + "debug";
private static final String PLUGIN_URL = "slinglogback";
private static final String PRINTER_URL = "slinglogbacklogs";
private static final String RESET_EVENT_TOPIC = "org/apache/sling/commons/log/RESET";
private final String rootDir;
private final String contextName = "sling";
private final LogConfigManager logConfigManager;
private final List<LogbackResetListener> resetListeners = new ArrayList<LogbackResetListener>();
private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
/**
* Acts as a bridge between Logback and OSGi
*/
private final LoggerContextListener osgiIntegrationListener = new OsgiIntegrationListener();
private final boolean debug;
private final boolean started;
private final Semaphore resetLock = new Semaphore(1);
private final AtomicBoolean configChanged = new AtomicBoolean();
private final AppenderTracker appenderTracker;
private final ConfigSourceTracker configSourceTracker;
private final List<ServiceRegistration> registrations = new ArrayList<ServiceRegistration>();
/**
* Time at which reset started. Used as the threshold for logging error messages from status printer
*/
private volatile long resetStartTime;
public LogbackManager(BundleContext bundleContext) throws InvalidSyntaxException {
final long startTime = System.currentTimeMillis();
setLoggerContext((LoggerContext) LoggerFactory.getILoggerFactory());
this.rootDir = getRootDir(bundleContext);
this.debug = Boolean.parseBoolean(bundleContext.getProperty(DEBUG));
this.appenderTracker = new AppenderTracker(bundleContext,getLoggerContext());
this.configSourceTracker = new ConfigSourceTracker(bundleContext,this);
//TODO Make it configurable
getLoggerContext().setName(contextName);
this.logConfigManager = new LogConfigManager(getLoggerContext(),bundleContext, rootDir,this);
resetListeners.add(logConfigManager);
resetListeners.add(appenderTracker);
resetListeners.add(configSourceTracker);
getLoggerContext().addListener(osgiIntegrationListener);
configure();
registerWebConsoleSupport(bundleContext);
registerEventHandler(bundleContext);
StatusPrinter.printInCaseOfErrorsOrWarnings(getLoggerContext(),startTime);
started = true;
}
public void shutdown() {
for(ServiceRegistration reg : registrations){
reg.unregister();
}
appenderTracker.close();
configSourceTracker.close();
getLoggerContext().removeListener(osgiIntegrationListener);
logConfigManager.close();
getLoggerContext().stop();
}
public void configChanged(){
if(!started){
return;
}
if(resetLock.tryAcquire()){
scheduleConfigReload();
}else{
configChanged.set(true);
addInfo("LoggerContext reset in progress. Marking config changed to true");
}
}
public LogConfigManager getLogConfigManager() {
return logConfigManager;
}
public AppenderTracker getAppenderTracker() {
return appenderTracker;
}
public ConfigSourceTracker getConfigSourceTracker() {
return configSourceTracker;
}
public void addSubsitutionProperties(InterpretationContext ic){
ic.addSubstitutionProperty("sling.home", rootDir);
}
public URL getDefaultConfig(){
return getClass().getClassLoader().getResource("logback-empty.xml");
}
private void configure() {
ConfiguratorCallback cb = new DefaultCallback();
//Check first for an explicit configuration file
File configFile = logConfigManager.getLogbackConfigFile();
if(configFile != null){
cb = new FilenameConfiguratorCallback(configFile);
}
configure(cb);
}
private void configure(ConfiguratorCallback cb) {
StatusListener statusListener = new StatusListenerAsList();
if(debug){
statusListener = new OnConsoleStatusListener();
}
getStatusManager().add(statusListener);
addInfo("Resetting context: " + getLoggerContext().getName());
resetContext(statusListener);
StatusUtil statusUtil = new StatusUtil(getLoggerContext());
JoranConfigurator configurator = createConfigurator();
final List<SaxEvent> eventList = configurator.recallSafeConfiguration();
final long threshold = System.currentTimeMillis();
try {
cb.perform(configurator);
if (statusUtil.hasXMLParsingErrors(threshold)) {
cb.fallbackConfiguration(eventList,createConfigurator(),statusListener);
}
addInfo("Context: " + getLoggerContext().getName() + " reloaded.");
} catch(JoranException je){
cb.fallbackConfiguration(eventList, createConfigurator(), statusListener);
} finally {
getStatusManager().remove(statusListener);
StatusPrinter.printInCaseOfErrorsOrWarnings(getLoggerContext(),resetStartTime);
}
}
private JoranConfigurator createConfigurator(){
SlingConfigurator configurator = new SlingConfigurator();
configurator.setContext(getLoggerContext());
return configurator;
}
private void resetContext(StatusListener statusListener){
getLoggerContext().reset();
// after a reset the statusListenerAsList gets removed as a listener
if(statusListener != null
&& !getStatusManager().getCopyOfStatusListenerList().contains(statusListener)){
getStatusManager().add(statusListener);
}
}
private void scheduleConfigReload(){
getLoggerContext().getExecutorService().submit(new LoggerReconfigurer());
}
private String getRootDir(BundleContext bundleContext) {
String rootDir = bundleContext.getProperty("sling.home");
if(rootDir == null){
rootDir = new File(".").getAbsolutePath();
}
addInfo("Using rootDir as "+rootDir);
return rootDir;
}
private class LoggerReconfigurer implements Runnable {
public void run() {
//TODO Might be better to run a job to monitor refreshRequirement
boolean configChanged = false;
try{
addInfo("Performing configuration");
configure();
configChanged = LogbackManager.this.configChanged.getAndSet(false);
if(configChanged){
scheduleConfigReload();
}
}catch(Exception e){
log.warn("Error occurred while re-configuring logger",e);
addError("Error occurred while re-configuring logger",e);
}finally{
if(!configChanged){
resetLock.release();
addInfo("Re configuration done");
}
}
}
}
//~-------------------------------LogggerContextListener
private class OsgiIntegrationListener implements LoggerContextListener {
public boolean isResetResistant() {
//The integration listener has to survive resets from other causes
//like reset when Logback detects change in config file and reloads on
//on its own in ReconfigureOnChangeFilter
return true;
}
public void onStart(LoggerContext context) {
}
public void onReset(LoggerContext context) {
addInfo("OsgiIntegrationListener : context reset detected. Adding LogManager to context map and firing" +
" listeners");
//Attach a console appender to handle logging untill we configure one. This would be
//removed in LogConfigManager.reset
getLoggerContext()
.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)
.addAppender(logConfigManager.getDefaultAppender());
//Now record the time of reset with a default appender attached to root logger
resetStartTime = System.currentTimeMillis();
context.putObject(LogbackManager.class.getName(),LogbackManager.this);
for(LogbackResetListener l : resetListeners){
l.onReset(context);
}
}
public void onStop(LoggerContext context) {
}
public void onLevelChange(Logger logger, Level level) {
}
}
//~--------------------------------Configurator Base
private class SlingConfigurator extends JoranConfigurator {
@Override
protected void buildInterpreter() {
super.buildInterpreter();
addSubsitutionProperties(interpreter.getInterpretationContext());
}
}
//~--------------------------------Configuration Support
private abstract class ConfiguratorCallback {
abstract void perform(JoranConfigurator configurator) throws JoranException;
/**
* Logic based on ch.qos.logback.classic.turbo.ReconfigureOnChangeFilter.ReconfiguringThread
*/
public void fallbackConfiguration(List<SaxEvent> eventList, JoranConfigurator configurator,
StatusListener statusListener) {
URL mainURL = getMainUrl();
if (mainURL != null) {
if (eventList != null) {
addWarn("Falling back to previously registered safe configuration.");
try {
resetContext(statusListener);
GenericConfigurator.informContextOfURLUsedForConfiguration(context, mainURL);
configurator.doConfigure(eventList);
addInfo("Re-registering previous fallback configuration once more as a fallback configuration point");
configurator.registerSafeConfiguration();
} catch (JoranException e) {
addError("Unexpected exception thrown by a configuration considered safe.", e);
}
} else {
addWarn("No previous configuration to fall back on.");
}
}
}
protected URL getMainUrl() {
return null;
}
}
private class FilenameConfiguratorCallback extends ConfiguratorCallback {
private final File configFile;
public FilenameConfiguratorCallback(File configFile) {
this.configFile = configFile;
}
public void perform(JoranConfigurator configurator) throws JoranException {
final String path = configFile.getAbsolutePath();
addInfo("Configuring from " + path);
if (configFile.getName().endsWith("xml")) {
configurator.doConfigure(configFile);
} else if (configFile.getName().endsWith("groovy")) {
if (EnvUtil.isGroovyAvailable()) {
// avoid directly referring to GafferConfigurator so as to avoid
// loading groovy.lang.GroovyObject . See also http://jira.qos.ch/browse/LBCLASSIC-214
GafferUtil.runGafferConfiguratorOn(getLoggerContext(), this, configFile);
} else {
addError("Groovy classes are not available on the class path. ABORTING INITIALIZATION.");
}
}
}
@Override
protected URL getMainUrl() {
try {
return configFile.toURI().toURL();
} catch (MalformedURLException e) {
addWarn("Cannot convert file to url " + configFile.getAbsolutePath(),e);
return null;
}
}
}
private class DefaultCallback extends ConfiguratorCallback {
public void perform(JoranConfigurator configurator) throws JoranException {
configurator.doConfigure(getMainUrl());
}
@Override
protected URL getMainUrl() {
return getDefaultConfig();
}
}
//~ ----------------------------------------------WebConsole Support
public LoggerStateContext determineLoggerState(){
final List<Logger> loggers = getLoggerContext().getLoggerList();
final LoggerStateContext ctx = new LoggerStateContext(loggers);
for(Logger logger : loggers){
if(logger.iteratorForAppenders().hasNext() || logger.getLevel() != null){
ctx.loggerInfos.add(logger);
}
Iterator<Appender<ILoggingEvent>> itr = logger.iteratorForAppenders();
while(itr.hasNext()){
Appender<ILoggingEvent> a = itr.next();
if(a.getName() != null && !ctx.appenders.containsKey(a.getName())){
ctx.appenders.put(a.getName(),a);
}
}
}
return ctx;
}
public class LoggerStateContext {
final LoggerContext loggerContext = getLoggerContext();
final List<Logger> allLoggers;
/**
* List of logger which have explicitly defined level or appenders set
*/
final List<Logger> loggerInfos = new ArrayList<Logger>();
final Map<String,Appender<ILoggingEvent>> appenders = new HashMap<String, Appender<ILoggingEvent>>();
final Map<Appender<ILoggingEvent>, AppenderTracker.AppenderInfo> dynamicAppenders =
new HashMap<Appender<ILoggingEvent>, AppenderTracker.AppenderInfo>();
private LoggerStateContext(List<Logger> allLoggers) {
this.allLoggers = allLoggers;
for(AppenderTracker.AppenderInfo ai : getAppenderTracker().getAppenderInfos()){
dynamicAppenders.put(ai.appender, ai);
}
}
int getNumberOfLoggers(){
return allLoggers.size();
}
int getNumOfDynamicAppenders(){
return getAppenderTracker().getAppenderInfos().size();
}
int getNumOfAppenders(){
return appenders.size();
}
boolean isDynamicAppender(Appender<ILoggingEvent> a){
return dynamicAppenders.containsKey(a);
}
Collection<Appender<ILoggingEvent>> getAllAppenders(){
return appenders.values();
}
}
private void registerWebConsoleSupport(BundleContext context){
final ServiceFactory serviceFactory = new PluginServiceFactory(PLUGIN_URL);
Properties pluginProps = new Properties();
pluginProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
pluginProps.put(Constants.SERVICE_DESCRIPTION, "Sling Log Support");
pluginProps.put("felix.webconsole.label", PLUGIN_URL);
pluginProps.put("felix.webconsole.title", "Sling Log Support");
pluginProps.put("felix.webconsole.css", new String[]{
"/"+PLUGIN_URL+"/res/ui/prettify.css",
"/"+PLUGIN_URL+"/res/ui/log.css"
});
registrations.add(context.registerService("javax.servlet.Servlet",serviceFactory, pluginProps));
Properties printerProps = new Properties();
printerProps.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
printerProps.put(Constants.SERVICE_DESCRIPTION, "Sling Log Configuration Printer");
printerProps.put("felix.webconsole.label", PRINTER_URL);
printerProps.put("felix.webconsole.title", "Log Files");
printerProps.put("felix.webconsole.configprinter.modes", "always");
//TODO need to see to add support for Inventory Feature
registrations.add(context.registerService(SlingConfigurationPrinter.class.getName(),
new SlingConfigurationPrinter(this), printerProps));
}
private class PluginServiceFactory implements ServiceFactory {
private Object instance;
private final String label;
private PluginServiceFactory(String label) {
this.label = label;
}
public Object getService(Bundle bundle, ServiceRegistration registration) {
synchronized (this) {
if (this.instance == null) {
this.instance = new SlingLogPanel(LogbackManager.this,label);
}
return instance;
}
}
public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
}
}
private void registerEventHandler(BundleContext bundleContext) {
Properties props = new Properties();
props.put(Constants.SERVICE_VENDOR, "Apache Software Foundation");
props.put(Constants.SERVICE_DESCRIPTION, "Sling Log Reset Event Handler");
props.put("event.topics", new String[]{RESET_EVENT_TOPIC});
registrations.add(bundleContext.registerService("org.osgi.service.event.EventHandler",
new ServiceFactory(){
private Object instance;
@Override
public Object getService(Bundle bundle, ServiceRegistration serviceRegistration) {
synchronized (this) {
if (this.instance == null) {
this.instance = new ConfigResetRequestHandler(LogbackManager.this);
}
return instance;
}
}
@Override
public void ungetService(Bundle bundle, ServiceRegistration serviceRegistration, Object o) {
}
}, props));
}
}