Skip to content

Commit

Permalink
Merge pull request #1452 from Graylog2/logs-rest
Browse files Browse the repository at this point in the history
Make internal logs accessible via REST API call
(cherry picked from commit 866d6e3)
  • Loading branch information
dennisoelkers authored and Jochen Schalanda committed Oct 6, 2015
1 parent fa7d149 commit 23e94ec
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 9 deletions.
@@ -0,0 +1,62 @@
package org.graylog2.rest.models.system.loggers.responses;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import org.hibernate.validator.constraints.NotEmpty;
import org.joda.time.DateTime;

import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.util.Map;

@AutoValue
@JsonAutoDetect
public abstract class InternalLogMessage {
@JsonProperty
@NotEmpty
public abstract String message();

@JsonProperty("class_name")
@NotEmpty
public abstract String className();

@JsonProperty
@NotEmpty
public abstract String level();

@JsonProperty
@Nullable
public abstract String marker();

@JsonProperty
@NotNull
public abstract DateTime timestamp();

@JsonProperty
@Nullable
public abstract String throwable();

@JsonProperty("thread_name")
@NotEmpty
public abstract String threadName();

@JsonProperty
@NotNull
public abstract Map<String, String> context();

@JsonCreator
public static InternalLogMessage create(@JsonProperty("message") @NotEmpty String message,
@JsonProperty("class_name") @NotEmpty String className,
@JsonProperty("level") @NotEmpty String level,
@JsonProperty("marker") @Nullable String marker,
@JsonProperty("timestamp") @NotNull DateTime timestamp,
@JsonProperty("throwable") @Nullable String throwable,
@JsonProperty("thread_name") @NotEmpty String threadName,
@JsonProperty("context") @NotNull Map<String, String> context) {
return new AutoValue_InternalLogMessage(message, className, level, marker, timestamp, throwable, threadName, context);
}

}
@@ -0,0 +1,39 @@
/**
* 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.rest.models.system.loggers.responses;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;

import java.util.Collection;

@AutoValue
@JsonAutoDetect
public abstract class LogMessagesSummary {

@JsonProperty
public abstract Collection<InternalLogMessage> messages();

@JsonCreator
public static LogMessagesSummary create(@JsonProperty("messages") Collection<InternalLogMessage> messages) {
return new AutoValue_LogMessagesSummary(messages);
}

}
102 changes: 102 additions & 0 deletions graylog2-server/src/main/java/org/graylog2/log4j/MemoryAppender.java
@@ -0,0 +1,102 @@
/**
* 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.log4j;

import org.apache.commons.collections.buffer.CircularFifoBuffer;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.logging.log4j.core.util.Booleans;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
* A Log4J appender that keeps a configurable number of messages in memory. Used to make recent internal log messages
* available via the REST API.
*/
@Plugin(name = "Memory", category = "Core", elementType = "appender", printObject = true)
public class MemoryAppender extends AbstractAppender {

private CircularFifoBuffer buffer;
private int bufferSize;

protected MemoryAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions, int bufferSize) {
super(name, filter, layout, ignoreExceptions);
this.bufferSize = bufferSize;
this.buffer = new CircularFifoBuffer(bufferSize);
}

@PluginFactory
public static MemoryAppender createAppender(
@PluginElement("Layout") Layout<? extends Serializable> layout,
@PluginElement("Filter") final Filter filter,
@PluginAttribute("name") final String name,
@PluginAttribute(value = "bufferSize", defaultInt = 500) final String bufferSize,
@PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) final String ignore) {
if (name == null) {
LOGGER.error("No name provided for MemoryAppender");
return null;
}

if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}

final int size = Integer.parseInt(bufferSize);
final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
return new MemoryAppender(name, filter, layout, ignoreExceptions, size);
}


@Override
public void append(LogEvent event) {
buffer.add(event);
}

@Override
public void stop() {
super.stop();
buffer.clear();
}

public List<LogEvent> getLogMessages(int max) {
if (buffer == null) {
throw new IllegalStateException("Cannot return log messages: Appender is not initialized.");
}

final List<LogEvent> result = new ArrayList<>(max);
final Object[] messages = buffer.toArray();
for (int i = messages.length - 1; i >= 0 && i >= messages.length - max; i--) {
result.add((LogEvent) messages[i]);
}

return result;
}

public int getBufferSize() {
return bufferSize;
}
}
Expand Up @@ -26,34 +26,52 @@
import com.wordnik.swagger.annotations.ApiResponses;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.impl.ThrowableProxy;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.graylog2.log4j.MemoryAppender;
import org.graylog2.rest.models.system.loggers.responses.InternalLogMessage;
import org.graylog2.rest.models.system.loggers.responses.LogMessagesSummary;
import org.graylog2.rest.models.system.loggers.responses.LoggersSummary;
import org.graylog2.rest.models.system.loggers.responses.SingleLoggerSummary;
import org.graylog2.rest.models.system.loggers.responses.SingleSubsystemSummary;
import org.graylog2.rest.models.system.loggers.responses.SubsystemSummary;
import org.graylog2.shared.rest.resources.RestResource;
import org.graylog2.shared.security.RestPermissions;
import org.hibernate.validator.constraints.NotEmpty;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.LoggerFactory;

import javax.validation.constraints.Min;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

@RequiresAuthentication
@Api(value = "System/Loggers", description = "Internal Graylog loggers")
@Path("/system/loggers")
public class LoggersResource extends RestResource {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(LoggersResource.class);
private static final Map<String, Subsystem> SUBSYSTEMS = ImmutableMap.of(
private static final String MEMORY_APPENDER_NAME = "graylog-internal-logs";

private static final Map<String, Subsystem> SUBSYSTEMS = ImmutableMap.<String, Subsystem>of(
"graylog2", new Subsystem("Graylog2", "org.graylog2", "All messages from graylog2-owned systems."),
"indexer", new Subsystem("Indexer", "org.elasticsearch", "All messages related to indexing and searching."),
"authentication", new Subsystem("Authentication", "org.apache.shiro", "All user authentication messages."),
Expand Down Expand Up @@ -140,8 +158,8 @@ private void setLoggerLevel(final String loggerName, final Level level) {
})
@Path("/subsystems/{subsystem}/level/{level}")
public void setSubsystemLoggerLevel(
@ApiParam(name = "subsystem", required = true) @PathParam("subsystem") String subsystemTitle,
@ApiParam(name = "level", required = true) @PathParam("level") String level) {
@ApiParam(name = "subsystem", required = true) @PathParam("subsystem") @NotEmpty String subsystemTitle,
@ApiParam(name = "level", required = true) @PathParam("level") @NotEmpty String level) {
if (!SUBSYSTEMS.containsKey(subsystemTitle)) {
LOG.warn("No such subsystem: [{}]. Returning 404.", subsystemTitle);
throw new NotFoundException();
Expand All @@ -158,14 +176,74 @@ public void setSubsystemLoggerLevel(
notes = "Provided level is falling back to DEBUG if it does not exist")
@Path("/{loggerName}/level/{level}")
public void setSingleLoggerLevel(
@ApiParam(name = "loggerName", required = true) @PathParam("loggerName") String loggerName,
@ApiParam(name = "level", required = true) @PathParam("level") String level) {
@ApiParam(name = "loggerName", required = true) @PathParam("loggerName") @NotEmpty String loggerName,
@ApiParam(name = "level", required = true) @NotEmpty @PathParam("level") String level) {
checkPermission(RestPermissions.LOGGERS_EDIT, loggerName);
setLoggerLevel(loggerName, Level.toLevel(level.toUpperCase()));
}

private static class Subsystem {
@GET
@Timed
@ApiOperation(value = "Get recent internal log messages")
@ApiResponses(value = {
@ApiResponse(code = 404, message = "Memory appender is disabled."),
@ApiResponse(code = 500, message = "Memory appender is broken.")
})
@Path("/messages/recent")
@Produces(MediaType.APPLICATION_JSON)
public LogMessagesSummary messages(@ApiParam(name = "limit", value = "How many log messages should be returned", defaultValue = "500", allowableValues = "range[0, infinity]")
@QueryParam("limit") @DefaultValue("500") @Min(0L) int limit,
@ApiParam(name = "level", value = "Which log level (or higher) should the messages have", defaultValue = "ALL", allowableValues = "[OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL]")
@QueryParam("level") @DefaultValue("ALL") @NotEmpty String level) {
final Appender appender = getAppender(MEMORY_APPENDER_NAME);
if (appender == null) {
throw new NotFoundException("Memory appender is disabled. Please refer to the example log4j.xml file.");
}

if (!(appender instanceof MemoryAppender)) {
throw new InternalServerErrorException("Memory appender is not an instance of MemoryAppender. Please refer to the example log4j.xml file.");
}

final Level logLevel = Level.toLevel(level, Level.ALL);
final MemoryAppender memoryAppender = (MemoryAppender) appender;
final List<InternalLogMessage> messages = new ArrayList<>(limit);
for (LogEvent event : memoryAppender.getLogMessages(limit)) {
final Level eventLevel = event.getLevel();
if (!eventLevel.isMoreSpecificThan(logLevel)) {
continue;
}

final ThrowableProxy thrownProxy = event.getThrownProxy();
final String throwable;
if (thrownProxy == null) {
throwable = null;
} else {
throwable = thrownProxy.getExtendedStackTraceAsString();
}

final Marker marker = event.getMarker();
messages.add(InternalLogMessage.create(
event.getMessage().getFormattedMessage(),
event.getLoggerName(),
eventLevel.toString(),
marker == null ? null : marker.toString(),
new DateTime(event.getTimeMillis(), DateTimeZone.UTC),
throwable,
event.getThreadName(),
event.getContextMap()
));
}

return LogMessagesSummary.create(messages);
}

private Appender getAppender(final String appenderName) {
final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
final Configuration configuration = loggerContext.getConfiguration();
return configuration.getAppender(appenderName);
}

private static class Subsystem {
private final String title;
private final String category;
private final String description;
Expand Down
6 changes: 5 additions & 1 deletion graylog2-server/src/main/resources/log4j2.xml
@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Configuration packages="org.graylog2.log4j">
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d %-5p: %c - %m%n"/>
</Console>

<!-- Internal Graylog log appender. Please do not disable. This makes internal log messages available via REST calls. -->
<Memory name="graylog-internal-logs" bufferSize="500"/>
</Appenders>
<Loggers>
<!-- Application Loggers -->
Expand All @@ -24,6 +27,7 @@
<Logger name="kafka.log.OffsetIndex" level="warn"/>
<Root level="warn">
<AppenderRef ref="STDOUT"/>
<AppenderRef ref="graylog-internal-logs"/>
</Root>
</Loggers>
</Configuration>

0 comments on commit 23e94ec

Please sign in to comment.