Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'refactoring' to include raven-logback

  • Loading branch information...
commit 5f27ced254c2ae71d14cd3d0278fbcba6188e7e6 2 parents f4982f5 + 46868e1
@roam roam authored
View
1  pom.xml
@@ -58,6 +58,7 @@
<modules>
<module>raven</module>
<module>raven-log4j</module>
+ <module>raven-logback</module>
</modules>
<build>
View
73 raven-logback/README.md
@@ -0,0 +1,73 @@
+# Raven-Logback
+A [logback](http://logback.qos.ch/) appender passing messages along to [Sentry](http://www.getsentry.com/).
+
+## Maven dependencies
+````xml
+ <dependencies>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.7.2</version>
+ </dependency>
+
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>1.0.7</version>
+ </dependency>
+
+ <dependency>
+ <groupId>net.kencochrane</groupId>
+ <artifactId>raven-logback</artifactId>
+ <version>2.0-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+````
+
+## Using the logback appender
+
+````java
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Example {
+ public static Logger logger = LoggerFactory.getLogger(Example.class);
+
+ public static void main(String[] args) {
+ logger.info("Hello World");
+ logger.trace("Hello World!");
+ logger.debug("How are you today?");
+ logger.info("I am fine.");
+ logger.warn("I love programming.");
+ logger.error("I am programming.");
+ }
+
+}
+````
+
+## Appender configuration
+Example of configuration in src/test/resources/sentryappender.logback.xml
+
+````xml
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %boldRed(%-5level) %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="SENTRY" class="net.kencochrane.raven.logback.SentryAppender">
+ <sentryDsn>http://2d0fd0e8c0d546279c7115b563bdf60f:3429a634a51b4b5b937be06d83bc6c97@localhost:9000/1</sentryDsn>
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT" />
+ <appender-ref ref="SENTRY" />
+ </root>
+</configuration>
+````
View
72 raven-logback/pom.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>net.kencochrane</groupId>
+ <artifactId>raven-all</artifactId>
+ <version>2.0-SNAPSHOT</version>
+ </parent>
+ <artifactId>raven-logback</artifactId>
+ <packaging>jar</packaging>
+ <name>raven-logback</name>
+ <description>Logback appender for Raven/Sentry.</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>raven</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-core</artifactId>
+ <version>1.0.7</version>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>1.0.7</version>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.8.1</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>1.7.2</version>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <version>2.3</version>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id> <!-- this is used for inheritance merges -->
+ <phase>package</phase> <!-- bind to the packaging phase -->
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
View
39 raven-logback/src/main/java/net/kencochrane/raven/logback/LogbackMDC.java
@@ -0,0 +1,39 @@
+package net.kencochrane.raven.logback;
+
+import net.kencochrane.raven.spi.RavenMDC;
+
+import org.slf4j.MDC;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+
+public class LogbackMDC extends RavenMDC {
+
+ private static final ThreadLocal<ILoggingEvent> THREAD_LOGGING_EVENT = new ThreadLocal<ILoggingEvent>();
+
+ public void setThreadLoggingEvent(ILoggingEvent event) {
+ THREAD_LOGGING_EVENT.set(event);
+ }
+
+ public void removeThreadLoggingEvent() {
+ THREAD_LOGGING_EVENT.remove();
+ }
+
+ @Override
+ public Object get(String key) {
+ if (THREAD_LOGGING_EVENT.get() != null) {
+ return THREAD_LOGGING_EVENT.get().getMDCPropertyMap().get(key);
+ }
+ return MDC.get(key);
+ }
+
+ @Override
+ public void put(String key, Object value) {
+ MDC.put(key, value.toString());
+ }
+
+ @Override
+ public void remove(String key) {
+ MDC.remove(key);
+ }
+
+}
View
188 raven-logback/src/main/java/net/kencochrane/raven/logback/SentryAppender.java
@@ -0,0 +1,188 @@
+package net.kencochrane.raven.logback;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.kencochrane.raven.Client;
+import net.kencochrane.raven.SentryDsn;
+import net.kencochrane.raven.spi.JSONProcessor;
+import net.kencochrane.raven.spi.RavenMDC;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import ch.qos.logback.core.AppenderBase;
+import ch.qos.logback.core.encoder.Encoder;
+
+/**
+ * Logback appender that will send messages to Sentry.
+ */
+public class SentryAppender extends AppenderBase<ILoggingEvent> {
+
+ private boolean async;
+ private LogbackMDC mdc;
+ protected String sentryDsn;
+ protected Client client;
+ protected boolean messageCompressionEnabled = true;
+ private List<JSONProcessor> jsonProcessors = Collections.emptyList();
+ private Encoder<ILoggingEvent> encoder;
+
+ public SentryAppender() {
+ initMDC();
+ mdc = (LogbackMDC) RavenMDC.getInstance();
+ }
+
+ public boolean isAsync() {
+ return async;
+ }
+
+ public void setAsync(boolean async) {
+ this.async = async;
+ }
+
+ public String getSentryDsn() {
+ return sentryDsn;
+ }
+
+ public void setSentryDsn(String sentryDsn) {
+ this.sentryDsn = sentryDsn;
+ }
+
+ public boolean isMessageCompressionEnabled() {
+ return messageCompressionEnabled;
+ }
+
+ public void setMessageCompressionEnabled(boolean messageCompressionEnabled) {
+ this.messageCompressionEnabled = messageCompressionEnabled;
+ }
+
+ /**
+ * Set a comma-separated list of fully qualified class names of
+ * JSONProcessors to be used.
+ *
+ * @param setting
+ * a comma-separated list of fully qualified class names of
+ * JSONProcessors
+ */
+ public void setJsonProcessors(String setting) {
+ this.jsonProcessors = loadJSONProcessors(setting);
+ }
+
+ /**
+ * Notify processors that a message has been logged. Note that this method
+ * is intended to be run on the same thread that creates the message.
+ */
+ public void notifyProcessorsBeforeAppending() {
+ for (JSONProcessor processor : jsonProcessors) {
+ processor.prepareDiagnosticContext();
+ }
+ }
+
+ /**
+ * Notify processors after a message has been logged. Note that this method
+ * is intended to be run on the same thread that creates the message.
+ */
+ public void notifyProcessorsAfterAppending() {
+ for (JSONProcessor processor : jsonProcessors) {
+ processor.clearDiagnosticContext();
+ }
+ }
+
+ @Override
+ public void start() {
+ activateOptions();
+ super.start();
+ }
+
+ @Override
+ public void stop() {
+ if (client != null) {
+ client.stop();
+ }
+ super.stop();
+ }
+
+ public void activateOptions() {
+ client = sentryDsn == null ? new Client() : new Client(SentryDsn.buildOptional(sentryDsn));
+ client.setJSONProcessors(jsonProcessors);
+ client.setMessageCompressionEnabled(messageCompressionEnabled);
+ }
+
+ @Override
+ protected void append(ILoggingEvent event) {
+ mdc.setThreadLoggingEvent(event);
+ try {
+ // get timestamp and timestamp in correct string format.
+ long timestamp = event.getTimeStamp();
+
+ // get the log and info about the log.
+ String message = event.getMessage();
+ String logger = event.getLoggerName();
+ int level = event.getLevel().toInt() / 1000; // Need to divide by
+ // 1000 to keep
+ // consistent with
+ // sentry
+ String culprit = event.getLoggerName();
+
+ IThrowableProxy throwable = event.getThrowableProxy();
+
+ // notify processors about the message
+ // (in async mode this is done by AsyncSentryAppender)
+ if (!async) {
+ notifyProcessorsBeforeAppending();
+ }
+
+ // send the message to the sentry server
+ if (event.getThrowableProxy() == null) {
+ client.captureMessage(message, timestamp, logger, level, culprit);
+ } else {
+ client.captureException(message, timestamp, logger, level, culprit, ((ThrowableProxy) throwable).getThrowable());
+ }
+
+ if (!async) {
+ notifyProcessorsAfterAppending();
+ }
+ } finally {
+ mdc.removeThreadLoggingEvent();
+ }
+ }
+
+ private static List<JSONProcessor> loadJSONProcessors(String setting) {
+ if (setting == null) {
+ return Collections.emptyList();
+ }
+ try {
+ List<JSONProcessor> processors = new ArrayList<JSONProcessor>();
+ String[] clazzes = setting.split(",\\s*");
+ for (String clazz : clazzes) {
+ JSONProcessor processor = (JSONProcessor) Class.forName(clazz).newInstance();
+ processors.add(processor);
+ }
+ return processors;
+ } catch (ClassNotFoundException exception) {
+ throw new RuntimeException("Processor could not be found.", exception);
+ } catch (InstantiationException exception) {
+ throw new RuntimeException("Processor could not be instantiated.", exception);
+ } catch (IllegalAccessException exception) {
+ throw new RuntimeException("Processor could not be instantiated.", exception);
+ }
+ }
+
+ public static void initMDC() {
+ if (RavenMDC.getInstance() != null) {
+ if (!(RavenMDC.getInstance() instanceof LogbackMDC)) {
+ throw new IllegalStateException("An incompatible RavenMDC instance has been set. Please check your Raven configuration.");
+ }
+ return;
+ }
+ RavenMDC.setInstance(new LogbackMDC());
+ }
+
+ public Encoder<ILoggingEvent> getEncoder() {
+ return encoder;
+ }
+
+ public void setEncoder(Encoder<ILoggingEvent> encoder) {
+ this.encoder = encoder;
+ }
+}
View
235 raven-logback/src/test/java/net/kencochrane/raven/logback/SentryAppenderTest.java
@@ -0,0 +1,235 @@
+package net.kencochrane.raven.logback;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+
+import net.kencochrane.raven.Utils;
+import net.kencochrane.raven.spi.JSONProcessor;
+import net.kencochrane.raven.spi.RavenMDC;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.core.joran.spi.JoranException;
+
+/**
+ * Test cases for {@link SentryAppender}.
+ */
+public class SentryAppenderTest {
+
+ protected static SentryMock sentry;
+
+ @BeforeClass
+ public static void beforeClass() throws SocketException {
+ sentry = new SentryMock();
+ }
+
+ @AfterClass
+ public static void afterClass() throws SocketException {
+ System.setProperty(Utils.SENTRY_DSN, "");
+ System.setProperty("logback.configurationFile", "");
+ sentry.stop();
+ }
+
+ @Test
+ public void noSentryDsn() {
+ ((LoggerContext) LoggerFactory.getILoggerFactory()).reset();
+ System.setProperty(Utils.SENTRY_DSN, "");
+ System.setProperty("logback.configurationFile", "sentryappender-no-dsn.logback.xml");
+ LoggerFactory.getLogger(this.getClass()).debug("No Sentry DSN, no messages");
+ }
+
+ @Test
+ public void invalidDsn() throws JoranException {
+ ((LoggerContext) LoggerFactory.getILoggerFactory()).reset();
+ System.setProperty(Utils.SENTRY_DSN, "INVALID");
+ configureLogback();
+ LoggerFactory.getLogger(this.getClass()).debug("Invalid Sentry DSN, no messages");
+ }
+
+ @Test
+ public void debugLevel() throws IOException, ParseException, JoranException {
+ final String loggerName = "omg.logger";
+ final long logLevel = (long) Level.DEBUG_INT / 1000;
+ final String projectId = "1";
+ final String message = "hi there!";
+
+ configureLogback(projectId);
+ LoggerFactory.getLogger(loggerName).debug(message);
+ verifyMessage(loggerName, logLevel, "1", message);
+ }
+
+ @Test
+ public void infoLevel() throws IOException, ParseException, JoranException {
+ final String loggerName = "dude.wheres.my.ride";
+ final long logLevel = (long) Level.INFO_INT / 1000;
+ final String projectId = "2";
+ final String message = "This message will self-destruct in 5...4...3...";
+
+ configureLogback(projectId);
+ LoggerFactory.getLogger(loggerName).info(message);
+ verifyMessage(loggerName, logLevel, projectId, message);
+ }
+
+ @Test
+ public void warnLevel() throws IOException, ParseException, JoranException {
+ final String loggerName = "org.apache.commons.httpclient.andStuff";
+ final long logLevel = (long) Level.WARN_INT / 1000;
+ final String projectId = "20";
+ final String message = "Warning! Warning! WARNING! Oh, come on!";
+
+ configureLogback(projectId);
+ LoggerFactory.getLogger(loggerName).warn(message);
+ verifyMessage(loggerName, logLevel, projectId, message);
+ }
+
+ @Test
+ public void errorLevel() throws IOException, ParseException, JoranException {
+ final String loggerName = "org.apache.commons.httpclient.andStuff";
+ final long logLevel = (long) Level.ERROR_INT / 1000;
+ final String projectId = "5";
+ final String message = "D'oh!";
+
+ configureLogback(projectId);
+ LoggerFactory.getLogger(loggerName).error(message);
+ verifyMessage(loggerName, logLevel, projectId, message);
+ }
+
+ @Test
+ public void errorLevel_withException() throws IOException, ParseException, JoranException {
+
+ final String loggerName = "org.apache.commons.httpclient.andStuff";
+ // When an exception is logged, the culprit should be the class+method
+ // where the exception occurred
+ final String culprit = getClass().getName() + ".errorLevel_withException";
+ final long logLevel = (long) Level.ERROR_INT / 1000;
+ final String projectId = "5";
+ final String message = "D'oh!";
+
+ configureLogback(projectId);
+ NullPointerException npe = new NullPointerException("Damn you!");
+
+ LoggerFactory.getLogger(loggerName).error(message, npe);
+
+ // Verify
+ JSONObject json = verifyMessage(culprit, logLevel, projectId, message);
+ JSONObject stacktrace = (JSONObject) json.get("sentry.interfaces.Stacktrace");
+ assertNotNull(stacktrace);
+ assertNotNull(stacktrace.get("frames"));
+ JSONArray frames = (JSONArray) stacktrace.get("frames");
+ assertTrue(frames.size() > 0);
+ JSONObject exception = (JSONObject) json.get("sentry.interfaces.Exception");
+ assertNotNull(exception);
+ assertEquals(NullPointerException.class.getSimpleName(), exception.get("type"));
+ assertEquals(npe.getMessage(), exception.get("value"));
+ assertEquals(NullPointerException.class.getPackage().getName(), exception.get("module"));
+ }
+
+ protected void setSentryDSN(String projectId) {
+ System.setProperty(Utils.SENTRY_DSN, String.format("udp://public:private@%s:%d/%s", sentry.host, sentry.port, projectId));
+ }
+
+ public void configureLogback(String projectId) throws JoranException {
+ setSentryDSN(projectId);
+ configureLogback();
+ }
+
+ public void configureLogback() throws JoranException {
+ LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+ JoranConfigurator jc = new JoranConfigurator();
+ jc.setContext(context);
+ context.reset();
+ jc.doConfigure(this.getClass().getClassLoader().getResource("sentryappender.logback.xml"));
+ }
+
+ protected JSONObject verifyMessage(String culprit, long logLevel, String projectId, String message) throws IOException, ParseException {
+ return verifyMessage(sentry, culprit, logLevel, projectId, message);
+ }
+
+ protected static JSONObject verifyMessage(SentryMock sentry, String culprit, long logLevel, String projectId, String message) throws IOException, ParseException {
+ JSONObject json = fetchJSONObject(sentry);
+ assertEquals(message, json.get("message"));
+ assertEquals(culprit, json.get("culprit"));
+ assertEquals(projectId, json.get("project"));
+ assertEquals(logLevel, json.get("level"));
+ return json;
+ }
+
+ protected static JSONObject fetchJSONObject(SentryMock sentry) throws IOException, ParseException {
+ String payload = Utils.fromUtf8(sentry.fetchMessage());
+ String[] payloadParts = StringUtils.split(payload, "\n\n");
+ assertEquals(2, payloadParts.length);
+ String raw = Utils.fromUtf8(Utils.decompress(Base64.decodeBase64(payloadParts[1])));
+ return (JSONObject) new JSONParser().parse(raw);
+ }
+
+ public static class SentryMock {
+ public final DatagramSocket serverSocket;
+ public final String host;
+ public final int port;
+
+ public SentryMock() throws SocketException {
+ this("localhost", 9506);
+ }
+
+ public SentryMock(String host, int port) throws SocketException {
+ this.host = host;
+ this.port = port;
+ serverSocket = new DatagramSocket(new InetSocketAddress(host, port));
+ }
+
+ public void stop() {
+ serverSocket.close();
+ }
+
+ public byte[] fetchMessage() throws IOException {
+ DatagramPacket packet = new DatagramPacket(new byte[10000], 10000);
+ serverSocket.receive(packet);
+ return packet.getData();
+ }
+
+ }
+
+ public static class MockJSONProcessor implements JSONProcessor {
+
+ private Long value = 0L;
+
+ @Override
+ public void prepareDiagnosticContext() {
+ // this is done to ensure prepareDiagnosticContext is called exactly
+ // once
+ value++;
+ }
+
+ @Override
+ public void clearDiagnosticContext() {
+ RavenMDC.getInstance().remove("test");
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void process(JSONObject json, Throwable exception) {
+ json.put("Test", value); // value should be 1
+ }
+
+ }
+
+}
View
20 raven-logback/src/test/resources/sentryappender-no-dsn.logback.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="SENTRY" class="net.kencochrane.raven.logback.SentryAppender">
+ <encoder>
+ <pattern>No Sentry DSN Test: %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT" />
+ <appender-ref ref="SENTRY" />
+ </root>
+</configuration>
View
21 raven-logback/src/test/resources/sentryappender.logback.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="SENTRY" class="net.kencochrane.raven.logback.SentryAppender">
+ <sentryDsn>${SENTRY_DSN}</sentryDsn>
+ <encoder>
+ <pattern>No Sentry DSN Test: %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+
+ <root level="debug">
+ <appender-ref ref="STDOUT" />
+ <appender-ref ref="SENTRY" />
+ </root>
+</configuration>
Please sign in to comment.
Something went wrong with that request. Please try again.