/**
* This file is part of Graylog.
*
* Graylog 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, either version 3 of the License, or
* (at your option) any later version.
*
* Graylog 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 Graylog. If not, see <http://www.gnu.org/licenses/>.
*/
package org.graylog2.bootstrap;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.log4j2.InstrumentedAppender;
import com.github.joschi.jadconfig.JadConfig;
import com.github.joschi.jadconfig.ParameterException;
import com.github.joschi.jadconfig.Repository;
import com.github.joschi.jadconfig.RepositoryException;
import com.github.joschi.jadconfig.ValidationException;
import com.github.joschi.jadconfig.guava.GuavaConverterFactory;
import com.github.joschi.jadconfig.guice.NamedConfigParametersModule;
import com.github.joschi.jadconfig.jodatime.JodaTimeConverterFactory;
import com.github.joschi.jadconfig.repositories.EnvironmentRepository;
import com.github.joschi.jadconfig.repositories.PropertiesRepository;
import com.github.joschi.jadconfig.repositories.SystemPropertiesRepository;
import com.github.rvesse.airline.annotations.Command;
import com.github.rvesse.airline.annotations.Option;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Binder;
import com.google.inject.CreationException;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Names;
import com.google.inject.spi.Message;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.graylog2.plugin.BaseConfiguration;
import org.graylog2.plugin.Plugin;
import org.graylog2.plugin.PluginConfigBean;
import org.graylog2.plugin.PluginLoaderConfig;
import org.graylog2.plugin.PluginMetaData;
import org.graylog2.plugin.PluginModule;
import org.graylog2.plugin.ServerStatus;
import org.graylog2.plugin.Tools;
import org.graylog2.plugin.Version;
import org.graylog2.plugin.system.NodeIdPersistenceException;
import org.graylog2.shared.UI;
import org.graylog2.shared.bindings.GuiceInjectorHolder;
import org.graylog2.shared.bindings.PluginBindings;
import org.graylog2.shared.plugins.ChainingClassLoader;
import org.graylog2.shared.plugins.PluginLoader;
import org.graylog2.shared.utilities.ExceptionUtils;
import org.jboss.netty.logging.InternalLoggerFactory;
import org.jboss.netty.logging.Slf4JLoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.google.common.base.Strings.nullToEmpty;
public abstract class CmdLineTool implements CliCommand {
static {
// Set up JDK Logging adapter, https://logging.apache.org/log4j/2.x/log4j-jul/index.html
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static final Logger LOG = LoggerFactory.getLogger(CmdLineTool.class);
protected static final Version version = Version.CURRENT_CLASSPATH;
protected static final String FILE_SEPARATOR = System.getProperty("file.separator");
protected static final String TMPDIR = System.getProperty("java.io.tmpdir", "/tmp");
protected final JadConfig jadConfig;
protected final BaseConfiguration configuration;
protected final ChainingClassLoader chainingClassLoader;
@Option(name = "--dump-config", description = "Show the effective Graylog configuration and exit")
protected boolean dumpConfig = false;
@Option(name = "--dump-default-config", description = "Show the default configuration and exit")
protected boolean dumpDefaultConfig = false;
@Option(name = {"-d", "--debug"}, description = "Run Graylog in debug mode")
private boolean debug = false;
@Option(name = {"-f", "--configfile"}, description = "Configuration file for Graylog")
private String configFile = "/etc/graylog/server/server.conf";
protected String commandName = "command";
protected Injector injector;
protected CmdLineTool(BaseConfiguration configuration) {
this(null, configuration);
}
protected CmdLineTool(String commandName, BaseConfiguration configuration) {
jadConfig = new JadConfig();
jadConfig.addConverterFactory(new GuavaConverterFactory());
jadConfig.addConverterFactory(new JodaTimeConverterFactory());
if (commandName == null) {
if (this.getClass().isAnnotationPresent(Command.class)) {
this.commandName = this.getClass().getAnnotation(Command.class).name();
} else {
this.commandName = "tool";
}
} else {
this.commandName = commandName;
}
this.configuration = configuration;
this.chainingClassLoader = new ChainingClassLoader(this.getClass().getClassLoader());
}
/**
* Validate the given configuration for this command.
*
* @return {@code true} if the configuration is valid, {@code false}.
*/
protected boolean validateConfiguration() {
return true;
}
public boolean isDumpConfig() {
return dumpConfig;
}
public boolean isDumpDefaultConfig() {
return dumpDefaultConfig;
}
public boolean isDebug() {
return debug;
}
protected abstract List<Module> getCommandBindings();
protected abstract List<Object> getCommandConfigurationBeans();
/**
* Things that have to run before the {@link #startCommand()} method is being called.
*/
protected void beforeStart() {}
@Override
public void run() {
final Level logLevel = setupLogger();
final PluginBindings pluginBindings = installPluginConfigAndBindings(getPluginPath(configFile), chainingClassLoader);
if (isDumpDefaultConfig()) {
dumpDefaultConfigAndExit();
}
final NamedConfigParametersModule configModule = readConfiguration(configFile);
if (isDumpConfig()) {
dumpCurrentConfigAndExit();
}
if (!validateConfiguration()) {
LOG.error("Validating configuration file failed - exiting.");
System.exit(1);
}
beforeStart();
final List<String> arguments = ManagementFactory.getRuntimeMXBean().getInputArguments();
LOG.info("Running with JVM arguments: {}", Joiner.on(' ').join(arguments));
injector = setupInjector(configModule, pluginBindings, binder -> binder.bind(ChainingClassLoader.class).toInstance(chainingClassLoader));
if (injector == null) {
LOG.error("Injector could not be created, exiting! (Please include the previous error messages in bug reports.)");
System.exit(1);
}
// This is holding all our metrics.
final MetricRegistry metrics = injector.getInstance(MetricRegistry.class);
addInstrumentedAppender(metrics, logLevel);
// Report metrics via JMX.
final JmxReporter reporter = JmxReporter.forRegistry(metrics).build();
reporter.start();
startCommand();
}
protected abstract void startCommand();
protected Level setupLogger() {
final Level logLevel;
if (isDebug()) {
LOG.info("Running in Debug mode");
logLevel = Level.DEBUG;
// Enable logging for Netty when running in debug mode.
InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory());
} else if (onlyLogErrors()) {
logLevel = Level.ERROR;
} else {
logLevel = Level.INFO;
}
initializeLogging(logLevel);
return logLevel;
}
private void initializeLogging(final Level logLevel) {
final LoggerContext context = (LoggerContext) LogManager.getContext(false);
final org.apache.logging.log4j.core.config.Configuration config = context.getConfiguration();
config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).setLevel(logLevel);
config.getLoggerConfig(Main.class.getPackage().getName()).setLevel(logLevel);
context.updateLoggers(config);
}
private void addInstrumentedAppender(final MetricRegistry metrics, final Level level) {
final InstrumentedAppender appender = new InstrumentedAppender(metrics, null, null, false);
appender.start();
final LoggerContext context = (LoggerContext) LogManager.getContext(false);
final org.apache.logging.log4j.core.config.Configuration config = context.getConfiguration();
config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME).addAppender(appender, level, null);
context.updateLoggers(config);
}
protected boolean onlyLogErrors() {
return false;
}
private void dumpCurrentConfigAndExit() {
System.out.println(dumpConfiguration(jadConfig.dump()));
System.exit(0);
}
private void dumpDefaultConfigAndExit() {
for (Object bean : getCommandConfigurationBeans())
jadConfig.addConfigurationBean(bean);
dumpCurrentConfigAndExit();
}
private PluginBindings installPluginConfigAndBindings(String pluginPath, ChainingClassLoader classLoader) {
final Set<Plugin> plugins = loadPlugins(pluginPath, classLoader);
final PluginBindings pluginBindings = new PluginBindings(plugins);
for (final Plugin plugin : plugins) {
for (final PluginModule pluginModule : plugin.modules()) {
for (final PluginConfigBean configBean : pluginModule.getConfigBeans()) {
jadConfig.addConfigurationBean(configBean);
}
}
}
return pluginBindings;
}
private String getPluginPath(String configFile) {
final PluginLoaderConfig pluginLoaderConfig = new PluginLoaderConfig();
processConfiguration(new JadConfig(getConfigRepositories(configFile), pluginLoaderConfig));
return pluginLoaderConfig.getPluginDir();
}
protected Set<Plugin> loadPlugins(String pluginPath, ChainingClassLoader chainingClassLoader) {
final File pluginDir = new File(pluginPath);
final Set<Plugin> plugins = new HashSet<>();
final PluginLoader pluginLoader = new PluginLoader(pluginDir, chainingClassLoader);
for (Plugin plugin : pluginLoader.loadPlugins()) {
final PluginMetaData metadata = plugin.metadata();
if (capabilities().containsAll(metadata.getRequiredCapabilities())) {
if (version.sameOrHigher(metadata.getRequiredVersion())) {
LOG.info("Loaded plugin: {}", plugin);
plugins.add(plugin);
} else {
LOG.error("Plugin \"" + metadata.getName() + "\" requires version " + metadata.getRequiredVersion() + " - not loading!");
}
} else {
LOG.debug("Skipping plugin \"{}\" because some capabilities are missing ({}).",
metadata.getName(),
Sets.difference(plugin.metadata().getRequiredCapabilities(), capabilities()));
}
}
return plugins;
}
protected Collection<Repository> getConfigRepositories(String configFile) {
return Arrays.asList(
new EnvironmentRepository("GRAYLOG_"),
new SystemPropertiesRepository("graylog."),
// Legacy prefixes
new EnvironmentRepository("GRAYLOG2_"),
new SystemPropertiesRepository("graylog2."),
new PropertiesRepository(configFile)
);
}
private String dumpConfiguration(final Map<String, String> configMap) {
final StringBuilder sb = new StringBuilder();
sb.append("# Configuration of graylog2-").append(commandName).append(" ").append(version).append(System.lineSeparator());
sb.append("# Generated on ").append(Tools.nowUTC()).append(System.lineSeparator());
for (Map.Entry<String, String> entry : configMap.entrySet()) {
sb.append(entry.getKey()).append('=').append(nullToEmpty(entry.getValue())).append(System.lineSeparator());
}
return sb.toString();
}
protected NamedConfigParametersModule readConfiguration(final String configFile) {
final List<Object> beans = getCommandConfigurationBeans();
for (Object bean : beans) {
jadConfig.addConfigurationBean(bean);
}
jadConfig.setRepositories(getConfigRepositories(configFile));
LOG.debug("Loading configuration from config file: {}", configFile);
processConfiguration(jadConfig);
return new NamedConfigParametersModule(jadConfig.getConfigurationBeans());
}
private void processConfiguration(JadConfig jadConfig) {
try {
jadConfig.process();
} catch (RepositoryException e) {
LOG.error("Couldn't load configuration: {}", e.getMessage());
System.exit(1);
} catch (ParameterException | ValidationException e) {
LOG.error("Invalid configuration", e);
System.exit(1);
}
}
protected List<Module> getSharedBindingsModules() {
return Lists.newArrayList();
}
protected Injector setupInjector(NamedConfigParametersModule configModule, Module... otherModules) {
try {
final ImmutableList.Builder<Module> modules = ImmutableList.builder();
modules.add(configModule);
modules.addAll(getSharedBindingsModules());
modules.addAll(getCommandBindings());
modules.addAll(Arrays.asList(otherModules));
modules.add(new Module() {
@Override
public void configure(Binder binder) {
binder.bind(String.class).annotatedWith(Names.named("BootstrapCommand")).toInstance(commandName);
}
});
return GuiceInjectorHolder.createInjector(modules.build());
} catch (CreationException e) {
annotateInjectorCreationException(e);
return null;
} catch (Exception e) {
LOG.error("Injector creation failed!", e);
return null;
}
}
protected void annotateInjectorCreationException(CreationException e) {
annotateInjectorExceptions(e.getErrorMessages());
throw e;
}
protected void annotateInjectorExceptions(Collection<Message> messages) {
for (Message message : messages) {
//noinspection ThrowableResultOfMethodCallIgnored
final Throwable rootCause = ExceptionUtils.getRootCause(message.getCause());
if (rootCause instanceof NodeIdPersistenceException) {
LOG.error(UI.wallString(
"Unable to read or persist your NodeId file. This means your node id file (" + configuration.getNodeIdFile() + ") is not readable or writable by the current user. The following exception might give more information: " + message));
System.exit(-1);
} else if (rootCause instanceof AccessDeniedException) {
LOG.error(UI.wallString("Unable to access file " + rootCause.getMessage()));
System.exit(-2);
} else {
// other guice error, still print the raw messages
// TODO this could potentially print duplicate messages depending on what a subclass does...
LOG.error("Guice error (more detail on log level debug): {}", message.getMessage());
if (rootCause != null) {
LOG.debug("Stacktrace:", rootCause);
}
}
}
}
protected Set<ServerStatus.Capability> capabilities() {
return Collections.emptySet();
}
}