package com.getsentry.raven.jul; 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.util.Util; import org.slf4j.MDC; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.ErrorManager; import java.util.logging.Filter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; /** * Logging handler in charge of sending the java.util.logging records to a Sentry server. */ public class SentryHandler extends Handler { /** * Name of the {@link Event#extra} property containing the Thread id. */ public static final String THREAD_ID = "Raven-ThreadId"; /** * 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; /** * If true, <code>String.format()</code> is used to render parameterized log * messages instead of <code>MessageFormat.format()</code>; Defaults to * false. */ protected boolean printfStyle; /** * 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; /** * Tags to add to every event. */ protected Map<String, String> tags = Collections.emptyMap(); /** * Set of tags to look for in the MDC. These will be added as tags to be sent to sentry. * <p> * Might be empty in which case no mapped tags are set. */ 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 SentryHandler. */ public SentryHandler() { retrieveProperties(); this.setFilter(new DropRavenFilter()); } /** * Creates an instance of SentryHandler. * * @param raven instance of Raven to use with this appender. */ public SentryHandler(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(); } } /** * Transforms a {@link Level} into an {@link Event.Level}. * * @param level original level as defined in JUL. * @return log level used within raven. */ protected static Event.Level getLevel(Level level) { if (level.intValue() >= Level.SEVERE.intValue()) { return Event.Level.ERROR; } else if (level.intValue() >= Level.WARNING.intValue()) { return Event.Level.WARNING; } else if (level.intValue() >= Level.INFO.intValue()) { return Event.Level.INFO; } else if (level.intValue() >= Level.ALL.intValue()) { return Event.Level.DEBUG; } else { return null; } } /** * 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> formattedParameters = new ArrayList<>(parameters.length); for (Object parameter : parameters) { formattedParameters.add((parameter != null) ? parameter.toString() : null); } return formattedParameters; } /** * Retrieves the properties of the logger. */ protected void retrieveProperties() { LogManager manager = LogManager.getLogManager(); String className = SentryHandler.class.getName(); String dsnProperty = manager.getProperty(className + ".dsn"); if (dsnProperty != null) { setDsn(dsnProperty); } String ravenFactoryProperty = manager.getProperty(className + ".ravenFactory"); if (ravenFactoryProperty != null) { setRavenFactory(ravenFactoryProperty); } String releaseProperty = manager.getProperty(className + ".release"); if (releaseProperty != null) { setRelease(releaseProperty); } String environmentProperty = manager.getProperty(className + ".environment"); if (environmentProperty != null) { setEnvironment(environmentProperty); } String serverNameProperty = manager.getProperty(className + ".serverName"); if (serverNameProperty != null) { setServerName(serverNameProperty); } String tagsProperty = manager.getProperty(className + ".tags"); if (tagsProperty != null) { setTags(tagsProperty); } String extraTagsProperty = manager.getProperty(className + ".extraTags"); if (extraTagsProperty != null) { setExtraTags(extraTagsProperty); } setPrintfStyle(Boolean.valueOf(manager.getProperty(className + ".printfStyle"))); } @Override public void publish(LogRecord record) { // Do not log the event if the current thread is managed by raven if (!isLoggable(record) || RavenEnvironment.isManagingThread()) { return; } RavenEnvironment.startManagingThread(); try { lazyInit(); Event event = buildEvent(record); raven.sendEvent(event); } catch (Exception e) { reportError("An exception occurred while creating a new event in Raven", e, ErrorManager.WRITE_FAILURE); } 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) { reportError("An exception occurred during the retrieval of the DSN for Raven", e, ErrorManager.OPEN_FAILURE); } catch (Exception e) { reportError("An exception occurred during the creation of a Raven instance", e, ErrorManager.OPEN_FAILURE); } } /** * Builds an Event based on the log record. * * @param record Log generated. * @return Event containing details provided by the logging system. */ protected Event buildEvent(LogRecord record) { EventBuilder eventBuilder = new EventBuilder() .withSdkName(RavenEnvironment.SDK_NAME + ":jul") .withLevel(getLevel(record.getLevel())) .withTimestamp(new Date(record.getMillis())) .withLogger(record.getLoggerName()); String message = record.getMessage(); if (record.getResourceBundle() != null && record.getResourceBundle().containsKey(record.getMessage())) { message = record.getResourceBundle().getString(record.getMessage()); } String topLevelMessage = message; if (record.getParameters() == null) { eventBuilder.withSentryInterface(new MessageInterface(message)); } else { String formatted; List<String> parameters = formatMessageParameters(record.getParameters()); try { formatted = formatMessage(message, record.getParameters()); topLevelMessage = formatted; // write out formatted as Event's message key } catch (Exception e) { // local formatting failed, send message and parameters without formatted string formatted = null; } eventBuilder.withSentryInterface(new MessageInterface(message, parameters, formatted)); } eventBuilder.withMessage(topLevelMessage); Throwable throwable = record.getThrown(); if (throwable != null) { eventBuilder.withSentryInterface(new ExceptionInterface(throwable)); } if (record.getSourceClassName() != null && record.getSourceMethodName() != null) { StackTraceElement fakeFrame = new StackTraceElement(record.getSourceClassName(), record.getSourceMethodName(), null, -1); eventBuilder.withCulprit(fakeFrame); } else { eventBuilder.withCulprit(record.getLoggerName()); } Map<String, String> mdc = MDC.getMDCAdapter().getCopyOfContextMap(); if (mdc != null) { for (Map.Entry<String, String> mdcEntry : mdc.entrySet()) { if (extraTags.contains(mdcEntry.getKey())) { eventBuilder.withTag(mdcEntry.getKey(), mdcEntry.getValue()); } else { eventBuilder.withExtra(mdcEntry.getKey(), mdcEntry.getValue()); } } } for (Map.Entry<String, String> tagEntry : tags.entrySet()) { eventBuilder.withTag(tagEntry.getKey(), tagEntry.getValue()); } eventBuilder.withExtra(THREAD_ID, record.getThreadID()); if (!Util.isNullOrEmpty(release)) { eventBuilder.withRelease(release.trim()); } if (!Util.isNullOrEmpty(environment)) { eventBuilder.withEnvironment(environment.trim()); } if (!Util.isNullOrEmpty(serverName)) { eventBuilder.withServerName(serverName.trim()); } raven.runBuilderHelpers(eventBuilder); return eventBuilder.build(); } /** * Returns formatted Event message when provided the message template and * parameters. * * @param message Message template body. * @param parameters Array of parameters for the message. * @return Formatted message. */ protected String formatMessage(String message, Object[] parameters) { String formatted; if (printfStyle) { formatted = String.format(message, parameters); } else { formatted = MessageFormat.format(message, parameters); } return formatted; } @Override public void flush() { } @Override public void close() throws SecurityException { RavenEnvironment.startManagingThread(); try { if (raven != null) { raven.closeConnection(); } } catch (Exception e) { reportError("An exception occurred while closing the Raven connection", e, ErrorManager.CLOSE_FAILURE); } finally { RavenEnvironment.stopManagingThread(); } } public void setDsn(String dsn) { this.dsn = dsn; } public void setPrintfStyle(boolean printfStyle) { this.printfStyle = printfStyle; } 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; } /** * Populates the tags map by parsing the given tags property string. * @param tags comma-delimited key-value pairs, e.g. * "tag1:value1,tag2:value2". */ public void setTags(String tags) { this.tags = Util.parseTags(tags); } public void setExtraTags(String extraTags) { this.extraTags = Util.parseExtraTags(extraTags); } private class DropRavenFilter implements Filter { @Override public boolean isLoggable(LogRecord record) { String loggerName = record.getLoggerName(); return loggerName == null || !loggerName.startsWith("com.getsentry.raven"); } } }