Package org.sonatype.nexus.internal.log

Source Code of org.sonatype.nexus.internal.log.LogbackLogManager$NexusLoggerContextListener

/*
* Sonatype Nexus (TM) Open Source Version
* Copyright (c) 2007-2014 Sonatype, Inc.
* All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0,
* which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks
* of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the
* Eclipse Foundation. All other trademarks are the property of their respective owners.
*/
package org.sonatype.nexus.internal.log;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.sonatype.nexus.configuration.application.ApplicationConfiguration;
import org.sonatype.nexus.log.LogConfiguration;
import org.sonatype.nexus.log.LogConfigurationCustomizer;
import org.sonatype.nexus.log.LogConfigurationCustomizer.Configuration;
import org.sonatype.nexus.log.LogConfigurationParticipant;
import org.sonatype.nexus.log.LogManager;
import org.sonatype.nexus.log.LoggerLevel;
import org.sonatype.nexus.proxy.events.NexusInitializedEvent;
import org.sonatype.nexus.util.file.FileSupport;
import org.sonatype.nexus.util.io.LimitedInputStream;
import org.sonatype.nexus.util.io.StreamSupport;
import org.sonatype.sisu.goodies.common.io.FileReplacer;
import org.sonatype.sisu.goodies.common.io.FileReplacer.ContentWriter;
import org.sonatype.sisu.goodies.eventbus.EventBus;

import org.slf4j.ILoggerFactory;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.classic.jul.LevelChangePropagator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.LoggerContextListener;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.helpers.NOPAppender;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.util.StatusPrinter;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Injector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.impl.StaticLoggerBinder;

import static com.google.common.base.Preconditions.checkNotNull;

//TODO configuration operations should be locking

/**
* Logback {@link LogManager}.
*/
@Singleton
@Named
public class LogbackLogManager
    implements LogManager
{
  private static final String JMX_DOMAIN = "org.sonatype.nexus.log";

  private static final String KEY_APPENDER_FILE = "appender.file";

  private static final String KEY_APPENDER_PATTERN = "appender.pattern";

  private static final String KEY_ROOT_LEVEL = "root.level";

  private static final String KEY_LOG_CONFIG_DIR = "nexus.log-config-dir";

  private static final String LOG_CONF = "logback.xml";

  private static final String LOG_CONF_PROPS = "logback.properties";

  private static final String PAX_BUNDLE_CONTEXT_KEY = "org.ops4j.pax.logging.logback.bundlecontext";

  private final Logger logger = LoggerFactory.getLogger(LogbackLogManager.class);

  private final Injector injector;

  private final ApplicationConfiguration applicationConfiguration;

  private final List<LogConfigurationParticipant> logConfigurationParticipants;

  private final List<LogConfigurationCustomizer> logConfigurationCustomizers;

  private final NexusLoggerContextListener loggerContextListener;

  private final EventBus eventBus;

  private final Map<String, LoggerLevel> overrides;

  private final Map<String, LoggerLevel> customisations;

  private ObjectName jmxName;

  @Inject
  public LogbackLogManager(final Injector injector,
                           final ApplicationConfiguration applicationConfiguration,
                           final List<LogConfigurationParticipant> logConfigurationParticipants,
                           final List<LogConfigurationCustomizer> logConfigurationCustomizers,
                           final EventBus eventBus)
  {
    this.injector = checkNotNull(injector);
    this.applicationConfiguration = checkNotNull(applicationConfiguration);
    this.logConfigurationParticipants = checkNotNull(logConfigurationParticipants);
    this.logConfigurationCustomizers = checkNotNull(logConfigurationCustomizers);
    this.loggerContextListener = new NexusLoggerContextListener();
    this.eventBus = checkNotNull(eventBus);
    this.overrides = Maps.newHashMap();
    this.customisations = Maps.newHashMap();
    try {
      jmxName = ObjectName.getInstance(JMX_DOMAIN, "name", LogManager.class.getSimpleName());
      final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
      server.registerMBean(new DefaultLogManagerMBean(this), jmxName);
    }
    catch (Exception e) {
      jmxName = null;
      logger.warn("Problem registering MBean for: " + getClass().getName(), e);
    }
    eventBus.register(this);
  }

  @Subscribe
  public void on(final NexusInitializedEvent evt) {
    configure();
  }

  private LoggerContext getLoggerContext() {
    ILoggerFactory factory = LoggerFactory.getILoggerFactory();
    if (factory instanceof LoggerContext) {
      return (LoggerContext) factory;
    }
    // temporary workaround for situations where SLF4j is not backed by logback
    return (LoggerContext) StaticLoggerBinder.getSingleton().getLoggerFactory();
  }

  @Override
  public synchronized void configure() {
    // TODO maybe do some optimization that if participants does not change, do not reconfigure
    prepareConfigurationFiles();
    readCustomisations();
    overrides.clear();
    File logOverridesConfigFile = getLogOverridesConfigFile();
    if (logOverridesConfigFile.exists()) {
      overrides.putAll(LogbackOverrides.read(logOverridesConfigFile));
    }
    mayInstallNexusLoggerContextListener();
    reconfigure();
  }

  @Override
  public synchronized void shutdown() {
    if (null != jmxName) {
      try {
        ManagementFactory.getPlatformMBeanServer().unregisterMBean(jmxName);
      }
      catch (final Exception e) {
        logger.warn("Problem unregistering MBean for: " + getClass().getName(), e);
      }
    }
    eventBus.unregister(this);
  }

  /**
   * @since 2.7
   */
  private File getLogConfigFile(final String name) {
    return new File(getLogConfigDir(), name);
  }

  /**
   * @since 2.7
   */
  private File getLogOverridesConfigFile() {
    return getLogConfigFile("logback-overrides.xml");
  }

  /**
   * @since 2.7
   */
  @Override
  public Map<String, LoggerLevel> getLoggers() {
    Map<String, LoggerLevel> loggers = Maps.newHashMap();

    LoggerContext loggerContext = getLoggerContext();
    for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
      String name = logger.getName();
      Level level = logger.getLevel();
      // only include loggers which explicit levels configured
      if (level != null) {
        loggers.put(name, convert(level));
      }
    }

    for (Entry<String, LoggerLevel> entry : customisations.entrySet()) {
      if (LoggerLevel.DEFAULT.equals(entry.getValue()) && !loggers.containsKey(entry.getKey())) {
        loggers.put(entry.getKey(), getLoggerEffectiveLevel(entry.getKey()));
      }
    }

    return loggers;
  }

  @Override
  public Set<File> getLogFiles() {
    HashSet<File> files = new HashSet<File>();

    LoggerContext ctx = getLoggerContext();

    for (Logger l : ctx.getLoggerList()) {
      ch.qos.logback.classic.Logger log = (ch.qos.logback.classic.Logger) l;
      Iterator<Appender<ILoggingEvent>> it = log.iteratorForAppenders();

      while (it.hasNext()) {
        Appender<ILoggingEvent> ap = it.next();

        if (ap instanceof FileAppender<?> || ap instanceof RollingFileAppender<?>) {
          FileAppender<?> fileAppender = (FileAppender<?>) ap;
          String path = fileAppender.getFile();
          files.add(new File(path));
        }
      }
    }

    return files;
  }

  @Override
  public File getLogFile(String filename) {
    Set<File> logFiles = getLogFiles();

    for (File logFile : logFiles) {
      if (logFile.getName().equals(filename)) {
        return logFile;
      }
    }

    return null;
  }

  @Override
  public LogConfiguration getConfiguration()
      throws IOException
  {
    Properties logProperties = loadConfigurationProperties();
    DefaultLogConfiguration configuration = new DefaultLogConfiguration();

    configuration.setRootLoggerLevel(logProperties.getProperty(KEY_ROOT_LEVEL));
    // TODO
    configuration.setRootLoggerAppenders("console,file");
    configuration.setFileAppenderPattern(logProperties.getProperty(KEY_APPENDER_PATTERN));
    configuration.setFileAppenderLocation(logProperties.getProperty(KEY_APPENDER_FILE));

    return configuration;
  }

  @Override
  public void setConfiguration(LogConfiguration configuration)
      throws IOException
  {
    Properties logProperties = loadConfigurationProperties();

    logProperties.setProperty(KEY_ROOT_LEVEL, configuration.getRootLoggerLevel());
    String pattern = configuration.getFileAppenderPattern();

    if (pattern == null) {
      pattern = getDefaultProperties().getProperty(KEY_APPENDER_PATTERN);
    }

    logProperties.setProperty(KEY_APPENDER_PATTERN, pattern);

    saveConfigurationProperties(logProperties);
    // TODO this will do a reconfiguration but would be just enough to "touch" logback.xml"
    reconfigure();
  }

  private Properties getDefaultProperties()
      throws IOException
  {
    Properties properties = new Properties();
    try (InputStream stream = this.getClass().getResourceAsStream(LOG_CONF_PROPS)) {
      properties.load(stream);
    }
    return properties;
  }

  /**
   * Retrieves a stream to the requested log file. This method ensures that the file is rooted in the log folder to
   * prevent browsing of the file system.
   *
   * @param logFile path of the file to retrieve
   * @return InputStream to the file or null if the file is not allowed or doesn't exist.
   */
  @Override
  @Nullable
  public InputStream getApplicationLogAsStream(String logFile, long from, long count) throws IOException {
    logger.debug("Retrieving log file: {}", logFile);

    if (logFile.contains(File.pathSeparator)) {
      logger.warn("Nexus refuses to retrieve log files with path separators in its name.");
      return null;
    }

    File log = getLogFile(logFile);
    if (log == null || !log.exists()) {
      logger.warn("Log file does not exist: {}", logFile);
      return null;
    }

    InputStream input = new BufferedInputStream(new FileInputStream(log));
    if (count >= 0) {
      return new LimitedInputStream(input, from, count);
    }
    else {
      return input;
    }
  }

  private Properties loadConfigurationProperties()
      throws IOException
  {
    prepareConfigurationFiles();
    String logConfigDir = getLogConfigDir();
    File logConfigPropsFile = new File(logConfigDir, LOG_CONF_PROPS);
    try (final InputStream in = new FileInputStream(logConfigPropsFile)) {
      Properties properties = new Properties();
      properties.load(in);
      return properties;
    }
  }

  private void saveConfigurationProperties(final Properties properties)
      throws IOException
  {
    final File configurationFile = new File(getLogConfigDir(), LOG_CONF_PROPS);
    logger.debug("Saving configuration: {}", configurationFile);
    final FileReplacer fileReplacer = new FileReplacer(configurationFile);
    // we save this file many times, don't litter backups
    fileReplacer.setDeleteBackupFile(true);
    fileReplacer.replace(new ContentWriter()
    {
      @Override
      public void write(final BufferedOutputStream output)
          throws IOException
      {
        properties.store(output, "Saved by Nexus");
      }
    });
  }

  private String getLogConfigDir() {
    String logConfigDir = System.getProperty(KEY_LOG_CONFIG_DIR);

    if (Strings.isNullOrEmpty(logConfigDir)) {
      logConfigDir = applicationConfiguration.getConfigurationDirectory().getAbsolutePath();

      System.setProperty(KEY_LOG_CONFIG_DIR, logConfigDir);
    }

    return logConfigDir;
  }

  private void prepareConfigurationFiles() {
    String logConfigDir = getLogConfigDir();

    File logConfigPropsFile = new File(logConfigDir, LOG_CONF_PROPS);
    if (!logConfigPropsFile.exists()) {
      try {
        URL configUrl = getClass().getResource(LOG_CONF_PROPS);
        try (final InputStream is = configUrl.openStream()) {
          FileSupport.copy(is, logConfigPropsFile.toPath());
        }
      }
      catch (IOException e) {
        throw new IllegalStateException("Could not create logback.properties as "
            + logConfigPropsFile.getAbsolutePath());
      }
    }

    if (logConfigurationParticipants != null) {
      for (final LogConfigurationParticipant participant : logConfigurationParticipants) {
        final String name = participant.getName();
        final File logConfigFile = new File(logConfigDir, name);
        if (participant instanceof LogConfigurationParticipant.NonEditable || !logConfigFile.exists()) {
          try {
            final FileReplacer fileReplacer = new FileReplacer(logConfigFile);
            // we save this file many times, don't litter backups
            fileReplacer.setDeleteBackupFile(true);
            fileReplacer.replace(new ContentWriter()
            {
              @Override
              public void write(final BufferedOutputStream output)
                  throws IOException
              {
                try (final InputStream in = participant.getConfiguration()) {
                  StreamSupport.copy(in, output, StreamSupport.BUFFER_SIZE);
                }
              }
            });
          }
          catch (IOException e) {
            throw new IllegalStateException(String.format("Could not create %s as %s", name,
                logConfigFile.getAbsolutePath()), e);
          }
        }
      }
    }
    final File logConfigFile = new File(logConfigDir, LOG_CONF);
    try {
      final FileReplacer fileReplacer = new FileReplacer(logConfigFile);
      // we save this file many times, don't litter backups
      fileReplacer.setDeleteBackupFile(true);
      fileReplacer.replace(new ContentWriter()
      {
        @Override
        public void write(final BufferedOutputStream output)
            throws IOException
        {
          try (final PrintWriter out = new PrintWriter(output)) {
            out.println("<?xml version='1.0' encoding='UTF-8'?>");
            out.println();
            out.println("<!--");
            out.println(
                "    DO NOT EDIT - This file aggregates log configuration from Nexus and its plugins, and is automatically generated.");
            out.println("-->");
            out.println();
            out.println("<configuration scan='true'>");
            out.println("  <property file='${nexus.log-config-dir}/logback.properties'/>");
            if (logConfigurationParticipants != null) {
              for (LogConfigurationParticipant participant : logConfigurationParticipants) {
                out.println(String.format(
                    "  <include file='${nexus.log-config-dir}/%s'/>", participant.getName())
                );
              }
            }
            File logOverridesConfigFile = getLogOverridesConfigFile();
            if (logOverridesConfigFile.exists()) {
              out.println(String.format(
                  "  <include file='${nexus.log-config-dir}/%s'/>", logOverridesConfigFile.getName())
              );
            }
            out.write("</configuration>");
          }
        }
      });
    }
    catch (IOException e) {
      throw new IllegalStateException("Could not create logback.xml as " + logConfigFile.getAbsolutePath());
    }
  }

  private void reconfigure() {
    String logConfigDir = getLogConfigDir();
    File file = new File(logConfigDir, LOG_CONF);
    logger.debug("Reconfiguring: {}", file);

    LoggerContext context = getLoggerContext();
    Object bundleContext = context.getObject(PAX_BUNDLE_CONTEXT_KEY);
    NOPAppender nopAppender = new NOPAppender();
    nopAppender.setContext(context);
    nopAppender.start();

    try {
      JoranConfigurator configurator = new JoranConfigurator();
      configurator.setContext(context);

      context.reset();

      // placeholder to avoid 'No appenders present' while reconfiguring
      context.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(nopAppender);

      // restore persisted setting so pax-logging can reload
      context.putObject(PAX_BUNDLE_CONTEXT_KEY, bundleContext);

      context.getStatusManager().clear();
      installNonResetResistantListeners();
      configurator.doConfigure(file);
    }
    catch (JoranException e) {
      e.printStackTrace();
    }

    StatusPrinter.printInCaseOfErrorsOrWarnings(context);
    injectAppenders();
  }

  /**
   * Invoked after {@link LoggerContext#reset()} by logger manager, to reinstall all listeners that are non
   * reset resistant.
   *
   * @since 2.8
   */
  private void installNonResetResistantListeners() {
    installLevelChangePropagator();
  }

  /**
   * Installs {@link LevelChangePropagator} in context.

   * @since 2.8
   */
  private void installLevelChangePropagator() {
    LoggerContext context = getLoggerContext();
    final LevelChangePropagator levelChangePropagator = new LevelChangePropagator();
    levelChangePropagator.setResetJUL(true);
    levelChangePropagator.setContext(context);
    context.addListener(levelChangePropagator);
  }


  private void injectAppenders() {
    LoggerContext ctx = getLoggerContext();

    for (Logger l : ctx.getLoggerList()) {
      ch.qos.logback.classic.Logger log = (ch.qos.logback.classic.Logger) l;
      Iterator<Appender<ILoggingEvent>> it = log.iteratorForAppenders();

      while (it.hasNext()) {
        Appender<ILoggingEvent> ap = it.next();
        injector.injectMembers(ap);
      }
    }
  }

  /**
   * Convert a Logback {@link Level} into a {@link LoggerLevel}.
   */
  private LoggerLevel convert(final Level level) {
    switch (level.toInt()) {

      case Level.ERROR_INT:
        return LoggerLevel.ERROR;

      case Level.WARN_INT:
        return LoggerLevel.WARN;

      case Level.INFO_INT:
        return LoggerLevel.INFO;

      case Level.DEBUG_INT:
        return LoggerLevel.DEBUG;

      case Level.OFF_INT:
        return LoggerLevel.OFF;

      default:
        return LoggerLevel.TRACE;
    }
  }

  /**
   * Convert a {@link LoggerLevel} into a Logback {@link Level}.
   */
  private Level convert(final LoggerLevel level) {
    return Level.valueOf(level.name());
  }

  @Override
  public void setLoggerLevel(final String name, final @Nullable LoggerLevel level) {
    if (level == null) {
      unsetLoggerLevel(name);
      return;
    }

    logger.debug("Set logger level: {}={}", name, level);
    LoggerLevel calculated = null;
    if (Logger.ROOT_LOGGER_NAME.equals(name)) {
      try {
        calculated = LoggerLevel.DEFAULT.equals(level) ? LoggerLevel.INFO : level;
        Properties logProperties = loadConfigurationProperties();
        logProperties.setProperty(KEY_ROOT_LEVEL, calculated.name());
        saveConfigurationProperties(logProperties);
      }
      catch (IOException e) {
        throw Throwables.propagate(e);
      }
    }
    else {
      if (LoggerLevel.DEFAULT.equals(level)) {
        boolean customizedByUser = overrides.containsKey(name) && !customisations.containsKey(name);
        unsetLoggerLevel(name);
        if (customizedByUser) {
          overrides.put(name, calculated = getLoggerEffectiveLevel(name));
          LogbackOverrides.write(getLogOverridesConfigFile(), overrides);
        }
        else {
          LoggerLevel customizedLevel = customisations.get(name);
          if (customizedLevel != null && !LoggerLevel.DEFAULT.equals(customizedLevel)) {
            calculated = customizedLevel;
          }
        }
      }
      else {
        overrides.put(name, calculated = level);
        LogbackOverrides.write(getLogOverridesConfigFile(), overrides);
      }
    }
    if (calculated != null) {
      setLogbackLoggerLevel(name, convert(calculated));
    }
  }

  @Override
  public void unsetLoggerLevel(final String name) {
    logger.debug("Unset logger level: {}", name);

    if (overrides.remove(name) != null) {
      LogbackOverrides.write(getLogOverridesConfigFile(), overrides);
    }
    if (Logger.ROOT_LOGGER_NAME.equals(name)) {
      setLoggerLevel(name, LoggerLevel.DEFAULT);
    }
    else {
      setLogbackLoggerLevel(name, null);
    }
  }

  @Override
  public void resetLoggers() {
    logger.debug("Resetting loggers");

    for (Map.Entry<String, LoggerLevel> entry : overrides.entrySet()) {
      if (!Logger.ROOT_LOGGER_NAME.equals(entry.getKey())) {
        setLogbackLoggerLevel(entry.getKey(), null);
      }
    }
    overrides.clear();
    LogbackOverrides.write(getLogOverridesConfigFile(), overrides);
    setLoggerLevel(Logger.ROOT_LOGGER_NAME, LoggerLevel.DEFAULT);
    applyCustomisations();

    logger.debug("Loggers reset to their default levels");
  }

  @Override
  @Nullable
  public LoggerLevel getLoggerLevel(final String name) {
    Level level = getLoggerContext().getLogger(name).getLevel();
    if (level != null) {
      return convert(level);
    }
    return null;
  }

  @Override
  public LoggerLevel getLoggerEffectiveLevel(final String name) {
    return convert(getLoggerContext().getLogger(name).getEffectiveLevel());
  }

  private void setLogbackLoggerLevel(final String name, final Level level) {
    getLoggerContext().getLogger(name).setLevel(level);
  }

  /**
   * Installs {@link NexusLoggerContextListener} if not already present in context.
   */
  private void mayInstallNexusLoggerContextListener() {
    LoggerContext context = getLoggerContext();
    if (!context.getCopyOfListenerList().contains(loggerContextListener)) {
      context.addListener(loggerContextListener);
      logger.debug("Nexus logger context listener installed");
    }
  }

  private void readCustomisations() {
    Configuration customizerConfiguration = new Configuration()
    {
      @Override
      public void setLoggerLevel(final String name, final LoggerLevel level) {
        customisations.put(checkNotNull(name), checkNotNull(level));
      }
    };
    customisations.clear();
    for (LogConfigurationCustomizer customizer : logConfigurationCustomizers) {
      customizer.customize(customizerConfiguration);
    }
  }

  private void applyCustomisations() {
    for (Entry<String, LoggerLevel> entry : customisations.entrySet()) {
      if (!LoggerLevel.DEFAULT.equals(entry.getValue())) {
        setLogbackLoggerLevel(entry.getKey(), convert(entry.getValue()));
      }
    }
  }

  private class NexusLoggerContextListener
      implements LoggerContextListener
  {

    @Override
    public boolean isResetResistant() {
      return true;
    }

    @Override
    public void onStart(final LoggerContext context) {
      // do nothing
    }

    @Override
    public void onReset(final LoggerContext context) {
      applyCustomisations();
    }

    @Override
    public void onStop(final LoggerContext context) {
      // do nothing
    }

    @Override
    public void onLevelChange(final ch.qos.logback.classic.Logger logger, final Level level) {
      // do nothing
    }
  }

}
TOP

Related Classes of org.sonatype.nexus.internal.log.LogbackLogManager$NexusLoggerContextListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.