/* * Copyright (c) 2012. HappyDroids LLC, All rights reserved. */ package net.kencochrane.sentry; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.*; import static org.apach3.commons.codec.binary.Base64.encodeBase64String; /** * User: ken cochrane * Date: 2/6/12 * Time: 11:59 AM */ public class RavenClient { private static final String RAVEN_JAVA_VERSION = "Raven-Java 0.6"; private RavenConfig config; private String sentryDSN; private String lastID; private MessageSender messageSender; public RavenClient() { this.sentryDSN = System.getenv("SENTRY_DSN"); if (this.sentryDSN == null || this.sentryDSN.length() == 0) { throw new RuntimeException("You must provide a DSN to RavenClient"); } setConfig(new RavenConfig(this.sentryDSN)); } public RavenClient(String sentryDSN) { this.sentryDSN = sentryDSN; setConfig(new RavenConfig(sentryDSN)); } public RavenClient(String sentryDSN, String proxy, boolean naiveSsl) { this.sentryDSN = sentryDSN; setConfig(new RavenConfig(sentryDSN, proxy, naiveSsl)); } public RavenConfig getConfig() { return config; } public void setConfig(RavenConfig config) { this.config = config; try { String protocol = config.getProtocol(); if ("udp".equals(protocol)) { messageSender = new UdpMessageSender(config, null); } else { URL endpoint = new URL(config.getSentryURL()); if (config.isNaiveSsl() && "https".equals(protocol)) { messageSender = new NaiveHttpsMessageSender(config, endpoint); } else { messageSender = new MessageSender(config, endpoint); } } } catch (MalformedURLException e) { throw new RuntimeException("Sentry URL is malformed", e); } } public String getSentryDSN() { return sentryDSN; } public void setSentryDSN(String sentryDSN) { this.sentryDSN = sentryDSN; } public void setLastID(String lastID) { this.lastID = lastID; } public String getLastID() { return lastID; } /** * Build up the JSON body for the POST that is sent to sentry * * @param message The log message * @param timestamp ISO8601 formatted date string * @param loggerClass The class associated with the log message * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param culprit Who we think caused the problem. * @param extras @return JSON String of message body */ private String buildJSON(String message, String timestamp, String loggerClass, int logLevel, String culprit, Throwable exception, JSONObject extras) { JSONObject obj = new JSONObject(); String lastID = RavenUtils.getRandomUUID(); obj.put("event_id", lastID); //Hexadecimal string representing a uuid4 value. obj.put("checksum", RavenUtils.calculateChecksum(message)); if (exception == null) { obj.put("culprit", culprit); } else { obj.put("culprit", determineCulprit(exception)); obj.put("sentry.interfaces.Exception", buildException(exception)); obj.put("sentry.interfaces.Stacktrace", buildStacktrace(exception)); } obj.put("timestamp", timestamp); obj.put("message", message); obj.put("project", getConfig().getProjectId()); obj.put("level", logLevel); obj.put("logger", loggerClass); obj.put("server_name", RavenUtils.getHostname()); if (extras != null) { obj.put("extra", extras); } setLastID(lastID); return obj.toJSONString(); } /** * Determines the class and method name where the root cause exception occurred. * * @param exception exception * @return the culprit */ private String determineCulprit(Throwable exception) { Throwable cause = exception; String culprit = null; while (cause != null) { StackTraceElement[] elements = cause.getStackTrace(); if (elements.length > 0) { StackTraceElement trace = elements[0]; culprit = trace.getClassName() + "." + trace.getMethodName(); } cause = cause.getCause(); } return culprit; } private JSONObject buildException(Throwable exception) { JSONObject json = new JSONObject(); json.put("type", exception.getClass().getSimpleName()); json.put("value", exception.getMessage()); json.put("module", exception.getClass().getPackage().getName()); return json; } private JSONObject buildStacktrace(Throwable exception) { JSONArray array = new JSONArray(); Throwable cause = exception; while (cause != null) { StackTraceElement[] elements = cause.getStackTrace(); for (int index = 0; index < elements.length; ++index) { if (index == 0) { JSONObject causedByFrame = new JSONObject(); String msg = "Caused by: " + cause.getClass().getName(); if (cause.getMessage() != null) { msg += " (\"" + cause.getMessage() + "\")"; } causedByFrame.put("filename", msg); causedByFrame.put("lineno", -1); array.add(causedByFrame); } StackTraceElement element = elements[index]; JSONObject frame = new JSONObject(); frame.put("filename", element.getClassName()); frame.put("function", element.getMethodName()); frame.put("lineno", element.getLineNumber()); array.add(frame); } cause = cause.getCause(); } JSONObject stacktrace = new JSONObject(); stacktrace.put("frames", array); return stacktrace; } /** * Take the raw message body and get it ready for sending. Encode and compress it. * * @param jsonMessage the message we want to prepare * @return Encode and compressed version of the jsonMessage */ private String buildMessageBody(String jsonMessage) { //need to zip and then base64 encode the message. // compressing doesn't work right now, sentry isn't decompressing correctly. // come back to it later. //return compressAndEncode(jsonMessage); // in the meantime just base64 encode it. return encodeBase64String(jsonMessage.getBytes()); } /** * Build up the JSON body and then Encode and compress it. * * @param message The log message * @param timestamp ISO8601 formatted date string * @param loggerClass The class associated with the log message * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param culprit Who we think caused the problem. * @param exception exception causing the problem * @param extras @return Encode and compressed version of the JSON Message body */ private String buildMessage(String message, String timestamp, String loggerClass, int logLevel, String culprit, Throwable exception, JSONObject extras) { // get the json version of the body String jsonMessage = buildJSON(message, timestamp, loggerClass, logLevel, culprit, exception, extras); // compress and encode the json message. return buildMessageBody(jsonMessage); } /** * Send the message to the sentry server. * * @param messageBody the encoded json message we are sending to the sentry server * @param timestamp the timestamp of the message */ private void sendMessage(String messageBody, long timestamp) { try { messageSender.send(messageBody, timestamp); } catch (IOException e) { // Eat the errors, we don't want to cause problems if there are major issues. e.printStackTrace(); } } /** * Send the log message to the sentry server. * <p/> * This method is deprecated. You should use captureMessage or captureException instead. * * @param theLogMessage The log message * @param timestamp unix timestamp * @param loggerClass The class associated with the log message * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param culprit Who we think caused the problem. * @param exception exception that occurred * @deprecated */ public void logMessage(String theLogMessage, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception) { String message = buildMessage(theLogMessage, RavenUtils.getTimestampString(timestamp), loggerClass, logLevel, culprit, exception, null); sendMessage(message, timestamp); } /** * Send the log message to the sentry server. * * @param message The log message * @param timestamp unix timestamp * @param loggerClass The class associated with the log message * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param culprit Who we think caused the problem. * @return lastID The ID for the last message. */ public String captureMessage(String message, long timestamp, String loggerClass, int logLevel, String culprit) { String body = buildMessage(message, RavenUtils.getTimestampString(timestamp), loggerClass, logLevel, culprit, null, null); sendMessage(body, timestamp); return getLastID(); } /** * Send the log message to the sentry server. * * @param message The log message * @return lastID The ID for the last message. */ public String captureMessage(String message) { return captureMessage(message, RavenUtils.getTimestampLong(), "root", 50, null); } /** * Send the exception to the sentry server. * * @param message The log message * @param timestamp unix timestamp * @param loggerClass The class associated with the log message * @param logLevel int value for Log level for message (DEBUG, ERROR, INFO, etc.) * @param culprit Who we think caused the problem. * @param exception exception that occurred */ public String captureException(String message, long timestamp, String loggerClass, int logLevel, String culprit, Throwable exception, JSONObject extra) { String body = buildMessage(message, RavenUtils.getTimestampString(timestamp), loggerClass, logLevel, culprit, exception, extra); sendMessage(body, timestamp); return getLastID(); } /** * Send an exception to the sentry server. * * @param exception exception that occurred * @return lastID The ID for the last message. */ public String captureException(Throwable exception) { return captureException(exception.getMessage(), RavenUtils.getTimestampLong(), "root", 50, null, exception, null); } public static class MessageSender { public final RavenConfig config; public final URL endpoint; public MessageSender(RavenConfig config, URL endpoint) { this.config = config; this.endpoint = endpoint; } public void send(String messageBody, long timestamp) throws IOException { // get the hmac Signature for the header String hmacSignature = RavenUtils.getSignature(messageBody, timestamp, config.getSecretKey()); // get the auth header String authHeader = buildAuthHeader(hmacSignature, timestamp, config.getPublicKey()); doSend(messageBody, authHeader); } protected void doSend(String messageBody, String authHeader) throws IOException { HttpURLConnection connection = getConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setReadTimeout(10000); connection.setRequestProperty("X-Sentry-Auth", authHeader); OutputStream output = connection.getOutputStream(); output.write(messageBody.getBytes()); output.close(); connection.connect(); InputStream input = connection.getInputStream(); input.close(); } /** * Build up the sentry auth header in the following format. * <p/> * The header is composed of a SHA1-signed HMAC, the timestamp from when the message was generated, and an * arbitrary client version string. The client version should be something distinct to your client, * and is simply for reporting purposes. * <p/> * X-Sentry-Auth: Sentry sentry_version=2.0, * sentry_signature=<hmac signature>, * sentry_timestamp=<signature timestamp>[, * sentry_key=<public api key>,[ * sentry_client=<client version, arbitrary>]] * * @param hmacSignature SHA1-signed HMAC * @param timestamp is the timestamp of which this message was generated * @param publicKey is either the public_key or the shared global key between client and server. * @return String version of the sentry auth header */ protected String buildAuthHeader(String hmacSignature, long timestamp, String publicKey) { StringBuilder header = new StringBuilder(); header.append("Sentry sentry_version=2.0,sentry_signature="); header.append(hmacSignature); header.append(",sentry_timestamp="); header.append(timestamp); header.append(",sentry_key="); header.append(publicKey); header.append(",sentry_client="); header.append(RAVEN_JAVA_VERSION); return header.toString(); } protected HttpURLConnection getConnection() throws IOException { return (HttpURLConnection) endpoint.openConnection(config.getProxy()); } } public static class NaiveHttpsMessageSender extends MessageSender { public final HostnameVerifier hostnameVerifier; public NaiveHttpsMessageSender(RavenConfig config, URL endpoint) { super(config, endpoint); this.hostnameVerifier = new AcceptAllHostnameVerifier(); } @Override protected HttpURLConnection getConnection() throws IOException { HttpsURLConnection connection = (HttpsURLConnection) endpoint.openConnection(config.getProxy()); connection.setHostnameVerifier(hostnameVerifier); return connection; } } public static class UdpMessageSender extends MessageSender { private final DatagramSocket socket; public UdpMessageSender(RavenConfig config, URL endpoint) { super(config, endpoint); try { socket = new DatagramSocket(); socket.connect(new InetSocketAddress(config.getHost(), config.getPort())); } catch (SocketException e) { throw new IllegalStateException(e); } } @Override protected void doSend(String messageBody, String authHeader) throws IOException { byte[] message = (authHeader + "\n\n" + messageBody).getBytes("UTF-8"); DatagramPacket packet = new DatagramPacket(message, message.length); socket.send(packet); } } public static class AcceptAllHostnameVerifier implements HostnameVerifier { @Override public boolean verify(String hostname, SSLSession sslSession) { return true; } } }