package com.getsentry.raven.logback;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import com.getsentry.raven.Raven;
import com.getsentry.raven.RavenFactory;
import com.getsentry.raven.config.Lookup;
import com.getsentry.raven.dsn.Dsn;
import com.getsentry.raven.dsn.InvalidDsnException;
import com.getsentry.raven.environment.RavenEnvironment;
import com.getsentry.raven.event.Event;
import com.getsentry.raven.event.EventBuilder;
import com.getsentry.raven.event.interfaces.ExceptionInterface;
import com.getsentry.raven.event.interfaces.MessageInterface;
import com.getsentry.raven.event.interfaces.SentryException;
import com.getsentry.raven.event.interfaces.StackTraceInterface;
import com.getsentry.raven.util.Util;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Appender for logback in charge of sending the logged events to a Sentry server.
*/
public class SentryAppender extends AppenderBase<ILoggingEvent> {
/**
* Name of the {@link Event#extra} property containing Maker details.
*/
public static final String LOGBACK_MARKER = "logback-Marker";
/**
* Name of the {@link Event#extra} property containing the Thread name.
*/
public static final String THREAD_NAME = "Raven-Threadname";
/**
* Current instance of {@link Raven}.
*
* @see #initRaven()
*/
protected volatile Raven raven;
/**
* DSN property of the appender.
* <p>
* Might be null in which case the DSN should be detected automatically.
*/
protected String dsn;
/**
* Name of the {@link RavenFactory} being used.
* <p>
* Might be null in which case the factory should be defined automatically.
*/
protected String ravenFactory;
/**
* Identifies the version of the application.
* <p>
* Might be null in which case the release information will not be sent with the event.
*/
protected String release;
/**
* Identifies the environment the application is running in.
* <p>
* Might be null in which case the environment information will not be sent with the event.
*/
protected String environment;
/**
* Server name to be sent to sentry.
* <p>
* Might be null in which case the hostname is found via a reverse DNS lookup.
*/
protected String serverName;
/**
* If set, only events with level = minLevel and up will be recorded. (This
* configuration parameter is deprecated in favor of using Logback
* Filters.)
*/
protected Level minLevel;
/**
* Additional tags to be sent to sentry.
* <p>
* Might be empty in which case no tags are sent.
*/
protected Map<String, String> tags = Collections.emptyMap();
/**
* Extras to use as tags.
*/
protected Set<String> extraTags = Collections.emptySet();
/**
* Used for lazy initialization of appender state, see {@link #lazyInit()}.
*/
private volatile boolean initialized = false;
/**
* Creates an instance of SentryAppender.
*/
public SentryAppender() {
this.addFilter(new DropRavenFilter());
}
/**
* Creates an instance of SentryAppender.
*
* @param raven instance of Raven to use with this appender.
*/
public SentryAppender(Raven raven) {
this();
this.raven = raven;
}
/**
* Do some appender initialization *after* instance construction, so that we don't
* log in the constructor (which can cause annoying messages) and so that system
* properties and environment variables override hardcoded appender configuration.
*/
@SuppressWarnings("checkstyle:hiddenfield")
private void lazyInit() {
if (!initialized) {
synchronized (this) {
if (!initialized) {
try {
String ravenFactory = Lookup.lookup("ravenFactory");
if (ravenFactory != null) {
setRavenFactory(ravenFactory);
}
String release = Lookup.lookup("release");
if (release != null) {
setRelease(release);
}
String environment = Lookup.lookup("environment");
if (environment != null) {
setEnvironment(environment);
}
String serverName = Lookup.lookup("serverName");
if (serverName != null) {
setServerName(serverName);
}
String tags = Lookup.lookup("tags");
if (tags != null) {
setTags(tags);
}
String extraTags = Lookup.lookup("extraTags");
if (extraTags != null) {
setExtraTags(extraTags);
}
} finally {
initialized = true;
}
}
}
}
if (raven == null) {
initRaven();
}
}
/**
* Extracts message parameters into a List of Strings.
* <p>
* null parameters are kept as null.
*
* @param parameters parameters provided to the logging system.
* @return the parameters formatted as Strings in a List.
*/
protected static List<String> formatMessageParameters(Object[] parameters) {
List<String> arguments = new ArrayList<>(parameters.length);
for (Object argument : parameters) {
arguments.add((argument != null) ? argument.toString() : null);
}
return arguments;
}
/**
* Transforms a {@link Level} into an {@link Event.Level}.
*
* @param level original level as defined in logback.
* @return log level used within raven.
*/
protected static Event.Level formatLevel(Level level) {
if (level.isGreaterOrEqual(Level.ERROR)) {
return Event.Level.ERROR;
} else if (level.isGreaterOrEqual(Level.WARN)) {
return Event.Level.WARNING;
} else if (level.isGreaterOrEqual(Level.INFO)) {
return Event.Level.INFO;
} else if (level.isGreaterOrEqual(Level.ALL)) {
return Event.Level.DEBUG;
} else {
return null;
}
}
/**
* {@inheritDoc}
* <p>
* The raven instance is started in this method instead of {@link #start()} in order to avoid substitute loggers
* being generated during the instantiation of {@link Raven}.<br>
* More on <a href="http://www.slf4j.org/codes.html#substituteLogger">www.slf4j.org/codes.html#substituteLogger</a>
*/
@Override
protected void append(ILoggingEvent iLoggingEvent) {
// Do not log the event if the current thread is managed by raven
if (RavenEnvironment.isManagingThread()) {
return;
}
RavenEnvironment.startManagingThread();
try {
if (minLevel != null && !iLoggingEvent.getLevel().isGreaterOrEqual(minLevel)) {
return;
}
lazyInit();
Event event = buildEvent(iLoggingEvent);
raven.sendEvent(event);
} catch (Exception e) {
addError("An exception occurred while creating a new event in Raven", e);
} finally {
RavenEnvironment.stopManagingThread();
}
}
/**
* Initialises the Raven instance.
*/
protected synchronized void initRaven() {
try {
if (dsn == null) {
dsn = Dsn.dsnLookup();
}
raven = RavenFactory.ravenInstance(new Dsn(dsn), ravenFactory);
} catch (InvalidDsnException e) {
addError("An exception occurred during the retrieval of the DSN for Raven", e);
} catch (Exception e) {
addError("An exception occurred during the creation of a Raven instance", e);
}
}
/**
* Builds an Event based on the logging event.
*
* @param iLoggingEvent Log generated.
* @return Event containing details provided by the logging system.
*/
protected Event buildEvent(ILoggingEvent iLoggingEvent) {
EventBuilder eventBuilder = new EventBuilder()
.withSdkName(RavenEnvironment.SDK_NAME + ":logback")
.withTimestamp(new Date(iLoggingEvent.getTimeStamp()))
.withMessage(iLoggingEvent.getFormattedMessage())
.withLogger(iLoggingEvent.getLoggerName())
.withLevel(formatLevel(iLoggingEvent.getLevel()))
.withExtra(THREAD_NAME, iLoggingEvent.getThreadName());
if (!Util.isNullOrEmpty(serverName)) {
eventBuilder.withServerName(serverName.trim());
}
if (!Util.isNullOrEmpty(release)) {
eventBuilder.withRelease(release.trim());
}
if (!Util.isNullOrEmpty(environment)) {
eventBuilder.withEnvironment(environment.trim());
}
if (iLoggingEvent.getArgumentArray() != null) {
eventBuilder.withSentryInterface(new MessageInterface(
iLoggingEvent.getMessage(),
formatMessageParameters(iLoggingEvent.getArgumentArray()),
iLoggingEvent.getFormattedMessage()));
}
if (iLoggingEvent.getThrowableProxy() != null) {
eventBuilder.withSentryInterface(new ExceptionInterface(extractExceptionQueue(iLoggingEvent)));
} else if (iLoggingEvent.getCallerData().length > 0) {
eventBuilder.withSentryInterface(new StackTraceInterface(iLoggingEvent.getCallerData()));
}
if (iLoggingEvent.getCallerData().length > 0) {
eventBuilder.withCulprit(iLoggingEvent.getCallerData()[0]);
} else {
eventBuilder.withCulprit(iLoggingEvent.getLoggerName());
}
for (Map.Entry<String, String> contextEntry : iLoggingEvent.getLoggerContextVO().getPropertyMap().entrySet()) {
eventBuilder.withExtra(contextEntry.getKey(), contextEntry.getValue());
}
for (Map.Entry<String, String> mdcEntry : iLoggingEvent.getMDCPropertyMap().entrySet()) {
if (extraTags.contains(mdcEntry.getKey())) {
eventBuilder.withTag(mdcEntry.getKey(), mdcEntry.getValue());
} else {
eventBuilder.withExtra(mdcEntry.getKey(), mdcEntry.getValue());
}
}
if (iLoggingEvent.getMarker() != null) {
eventBuilder.withTag(LOGBACK_MARKER, iLoggingEvent.getMarker().getName());
}
for (Map.Entry<String, String> tagEntry : tags.entrySet()) {
eventBuilder.withTag(tagEntry.getKey(), tagEntry.getValue());
}
raven.runBuilderHelpers(eventBuilder);
return eventBuilder.build();
}
/**
* Creates a sequence of {@link SentryException}s given a particular {@link ILoggingEvent}.
*
* @param iLoggingEvent Information detailing a particular logging event
*
* @return A {@link Deque} of {@link SentryException}s detailing the exception chain
*/
protected Deque<SentryException> extractExceptionQueue(ILoggingEvent iLoggingEvent) {
IThrowableProxy throwableProxy = iLoggingEvent.getThrowableProxy();
Deque<SentryException> exceptions = new ArrayDeque<>();
Set<IThrowableProxy> circularityDetector = new HashSet<>();
StackTraceElement[] enclosingStackTrace = new StackTraceElement[0];
//Stack the exceptions to send them in the reverse order
while (throwableProxy != null) {
if (!circularityDetector.add(throwableProxy)) {
addWarn("Exiting a circular exception!");
break;
}
StackTraceElement[] stackTraceElements = toStackTraceElements(throwableProxy);
StackTraceInterface stackTrace = new StackTraceInterface(stackTraceElements, enclosingStackTrace);
exceptions.push(createSentryExceptionFrom(throwableProxy, stackTrace));
enclosingStackTrace = stackTraceElements;
throwableProxy = throwableProxy.getCause();
}
return exceptions;
}
/**
* Given a {@link IThrowableProxy} and a {@link StackTraceInterface} return
* a {@link SentryException} to be reported to Sentry.
*
* @param throwableProxy Information detailing a Throwable
* @param stackTrace The stacktrace associated with the Throwable.
*
* @return A {@link SentryException} object ready to be sent to Sentry.
*/
protected SentryException createSentryExceptionFrom(IThrowableProxy throwableProxy,
StackTraceInterface stackTrace) {
String exceptionMessage = throwableProxy.getMessage();
String[] packageNameSimpleName = extractPackageSimpleClassName(throwableProxy.getClassName());
String exceptionPackageName = packageNameSimpleName[0];
String exceptionClassName = packageNameSimpleName[1];
return new SentryException(exceptionMessage, exceptionClassName, exceptionPackageName, stackTrace);
}
/**
* Given a {@link String} representing a classname, return Strings
* representing the package name and the class name individually.
*
* @param canonicalClassName A dotted-notation string representing a class name (eg. "java.util.Date")
*
* @return An array of {@link String}s. The first of which is the package name. The second is the class name.
*/
protected String[] extractPackageSimpleClassName(String canonicalClassName) {
String[] packageNameSimpleName = new String[2];
try {
Class<?> exceptionClass = Class.forName(canonicalClassName);
Package exceptionPackage = exceptionClass.getPackage();
packageNameSimpleName[0] = exceptionPackage != null ? exceptionPackage.getName()
: SentryException.DEFAULT_PACKAGE_NAME;
packageNameSimpleName[1] = exceptionClass.getSimpleName();
} catch (ClassNotFoundException e) {
int lastDot = canonicalClassName.lastIndexOf('.');
if (lastDot != -1) {
packageNameSimpleName[0] = canonicalClassName.substring(0, lastDot);
packageNameSimpleName[1] = canonicalClassName.substring(lastDot);
} else {
packageNameSimpleName[0] = SentryException.DEFAULT_PACKAGE_NAME;
packageNameSimpleName[1] = canonicalClassName;
}
}
return packageNameSimpleName;
}
/**
* Given a {@link IThrowableProxy} return an array of {@link StackTraceElement}s
* associated with the underlying {@link Throwable}.
*
* @param throwableProxy Information detailing a Throwable.
*
* @return The {@link StackTraceElement}s associated w/the underlying {@link Throwable}
*/
protected StackTraceElement[] toStackTraceElements(IThrowableProxy throwableProxy) {
StackTraceElementProxy[] stackTraceElementProxies = throwableProxy.getStackTraceElementProxyArray();
StackTraceElement[] stackTraceElements = new StackTraceElement[stackTraceElementProxies.length];
for (int i = 0, stackTraceElementsLength = stackTraceElementProxies.length; i < stackTraceElementsLength; i++) {
stackTraceElements[i] = stackTraceElementProxies[i].getStackTraceElement();
}
return stackTraceElements;
}
public void setDsn(String dsn) {
this.dsn = dsn;
}
public void setRavenFactory(String ravenFactory) {
this.ravenFactory = ravenFactory;
}
public void setRelease(String release) {
this.release = release;
}
public void setEnvironment(String environment) {
this.environment = environment;
}
public void setServerName(String serverName) {
this.serverName = serverName;
}
public void setMinLevel(String minLevel) {
this.minLevel = minLevel != null ? Level.toLevel(minLevel) : null;
}
/**
* Set the tags that should be sent along with the events.
*
* @param tags A String of tags. key/values are separated by colon(:) and tags are separated by commas(,).
*/
public void setTags(String tags) {
this.tags = Util.parseTags(tags);
}
/**
* Set the mapped extras that will be used to search MDC and upgrade key pair to a tag sent along with the events.
*
* @param extraTags A String of extraTags. extraTags are separated by commas(,).
*/
public void setExtraTags(String extraTags) {
this.extraTags = Util.parseExtraTags(extraTags);
}
@Override
public void stop() {
RavenEnvironment.startManagingThread();
try {
if (!isStarted()) {
return;
}
super.stop();
if (raven != null) {
raven.closeConnection();
}
} catch (Exception e) {
addError("An exception occurred while closing the Raven connection", e);
} finally {
RavenEnvironment.stopManagingThread();
}
}
private class DropRavenFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
String loggerName = event.getLoggerName();
if (loggerName != null && loggerName.startsWith("com.getsentry.raven")) {
return FilterReply.DENY;
}
return FilterReply.NEUTRAL;
}
}
}