Sending SMS for NewRelic alerts using the Twilio API

Our good friends at NewRelic offer a fine service for monitoring web apps and servers. However, for the unfortunate European customers, there doesn't seem to be a way to get the alerts via SMS. Because of this, I combined their nice webhook-feature with the powerful features of Twilio, a communication service provider, which - among other things - offers the possibility of sending SMS.

In this blogpost I'll walk over the java code I created to achieve this, a download of the entire project will also be available.

Overview

  • Maven setup of the project
  • Logback settings
  • Properties file
  • The web.xml
  • Sending SMS with the Twilio API
  • Capturing the webhooks from NewRelic
  • Converting the messages to SMS

Maven setup

For this project, we're using a pretty basic maven setup for a webapp. We'll be just compiling the code, and wrap it all up in a .war file. But, there are some important dependencies, which are needed to make it work. We're obviously using the twilio sdk, and rely on jersey to setup a REST server which will interact with the messages we get from NewRelic. Oh yes - we're logging our stuff with logback.

For those of you not using maven, here are the dependencies you'll need to include in your project:

Our complete pom is

Our complete pom is
<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>
    <groupId>eu.robusta</groupId>
    <artifactId>MonitoringWebhook</artifactId>
    <version>0.0.1</version>
    <packaging>war</packaging>
    <name>Robusta Monitoring Webhook</name>
    <description>A REST listener for NewRelic Webhooks, sending SMS via Twilio</description>
    <build>
        <sourceDirectory>src</sourceDirectory>
        <resources>
            <resource>
                <directory>src</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <warSourceDirectory>WebContent</warSourceDirectory>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
        <dependency>
            <groupId>com.twilio.sdk</groupId>
            <artifactId>twilio-java-sdk</artifactId>
            <version>3.3.15</version>
        </dependency>
        <!-- Required only when you are using JAX-RS Client -->
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-client</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-common</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-server</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <!-- if your container implements Servlet API older than 3.0, use "jersey-container-servlet-core" -->
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>7.0.42</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <issueManagement>
        <url>ht</url>
    </issueManagement>
    <organization>
        <name>Robusta</name>
        <url>https://www.robusta-hosting.eu</url>
    </organization>
</project>

Logback settings

Yes, I know, I should make some more of it, but for the sake of this demo, we're just using a basic stdout-logger, both for testing as for the real deal.

Our logback-test.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
    </layout>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Our logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
      <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
    </layout>
  </appender>
  
  <logger name="eu.robusta" level="TRACE"/>
 
  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Properties file

All of the client-specific stuff of our implementation is bundled in a single properties-file:

#Setup for your twilio account
twilio.account.sid=    #Your sid
twilio.auth.token=     #Your token
#Since we are using this number to send all messages, we might as well...
twilio.from.number=    #Your 'from' number (your number @ twilio)
twilio.to.numbers=     #A comma-delimited list of phone numbers which will be notified by the program

The web.xml

We have a REST listener to capture the alerts from NewRelic. To make it work, we'll have to configure it in the web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>MonitoringWebhook</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
   <servlet>
        <servlet-name>Robusta Monitoring Webhook</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>jersey.config.server.provider.packages</param-name>
            <param-value>eu.robusta.newrelic.listener</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Robusta Monitoring Webhook</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
</web-app>

Sending SMS with the Twilio API

Sending an SMS with the twilio sdk is pretty easy. What we're doing in this class, is reading out our properties file to get our credentials, the destination numbers, and the number which will be sending the numbers. For each destination number we'll be creating a Twilio text message (which is basically to, from and the body of the message), and send it out via Twilio's SmsFactory. 

Here's the code:

package eu.robusta.twilio;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.resource.factory.SmsFactory;
import com.twilio.sdk.resource.instance.Sms;

public class SMS {
    public static ArrayList sendMessage(String messageBody) throws IOException, TwilioRestException {
        Properties prop = new Properties();
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        InputStream stream = loader.getResourceAsStream("/monitoring.properties");
        prop.load(stream);
        TwilioRestClient client = new TwilioRestClient(prop.getProperty("twilio.account.sid"),
                prop.getProperty("twilio.auth.token"));

        String[] tos = prop.getProperty("twilio.to.numbers").split(",");
        ArrayList sids = new ArrayList();
        for (String to : tos) {
            Map params = new HashMap();
            params.put("Body", messageBody);
            params.put("To", to);
            params.put("From", prop.getProperty("twilio.from.number"));

            SmsFactory messageFactory = client.getAccount().getSmsFactory();
            Sms message = messageFactory.create(params);
            sids.add(message.getSid());
        }
        return sids;
    }
}

Capturing the webhooks from NewRelic

We're setting up a single REST listener to capture the alerts from NewRelic, which will handle both alerts and deployments, since NewRelic only supports one address for a web hook.

We're creating a simple listener, which will call the WebhookController to handle the alerts.

package eu.robusta.newrelic.listener;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import eu.robusta.controller.WebhookController;

@Path("/monitoring")
public class WebHook {
	@Path("/alert")
	@POST
	@Consumes({MediaType.APPLICATION_FORM_URLENCODED})
	public Response alert(@Context HttpHeaders headers, @FormParam("deployment")String deploymentString, @FormParam("alert")String alertString) {
		WebhookController ctrl = new WebhookController();
		ctrl.handleAlert(alertString, deploymentString);
		//return Response.noContent().build();
        return Response.ok().entity(new Boolean("true")).build();
	}
}

In the WebhookController, we capture the json request in a string, and use the JSONConverter class (see later) to convert it to a POJO, to be used in the SMS. We then convert the POJO (alert or deployment) to a text message via the SMSConverter (see later), and - of course - we send the text message...

package eu.robusta.controller;

import java.io.IOException;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

import com.twilio.sdk.TwilioRestException;

import eu.robusta.newrelic.object.Alert;
import eu.robusta.newrelic.object.Deployment;
import eu.robusta.twilio.SMS;
import eu.robusta.utils.JSONConverter;
import eu.robusta.utils.SMSConverter;

public class WebhookController {
	static final Logger LOG = LoggerFactory.getLogger(WebhookController.class);

	public void handleAlert(String alertString, String deployment) {
		LOG.trace("Handling the alert/deployment");
		try {
			if (alertString != null && alertString.length() > 0) {
				LOG.trace("Handling the alert");
				Alert alert = JSONConverter.convertAlert(alertString);
				String message = SMSConverter.convertAlert(alert);
				SMS.sendMessage(message);
			}
			if (deployment != null && deployment.length() > 0) {
				LOG.trace("Handling the deployment");
				Deployment deploy = JSONConverter.convertDeploy(deployment);
				String message = SMSConverter.convertDeploy(deploy);
				SMS.sendMessage(message);
			}
		} catch (JsonParseException e) {
			LOG.error(e.getLocalizedMessage(), e);
		} catch (JsonMappingException e) {
			LOG.error(e.getLocalizedMessage(), e);
		} catch (IOException e) {
			LOG.error(e.getLocalizedMessage(), e);
		} catch (TwilioRestException e) {
			LOG.error(e.getLocalizedMessage(), e);
		}
	}
}

Converting the messages

We're creating two simple Java classes to represent the two types of alerts we're getting from NewRelic. They are annotated to let jackson do it's work converting the JSON to a Java class. One for alerts:

package eu.robusta.newrelic.object;

import java.net.URL;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;

@XmlAccessorType(XmlAccessType.FIELD)
public class Alert {
    //alert: {
    //  "created_at":"2013-10-07T19:53:21+00:00",
    //  "application_name":"Application name",
    //  "account_name":"Account name",
    //  "severity":"critical",
    //  "message":"Apdex score fell below critical level of 0.90",
    //  "short_description":"[application name] alert opened",
    //  "long_description":"Alert opened on [application name]: Apdex score fell below critical level of 0.90",
    //  "alert_url":"https://rpm.newrelc.com/accounts/[account_id]/applications/[application_id]/incidents/[incident_id]"
    // }
	@XmlElement(name="created_at")
	private String created_at;
	@XmlElement(name="application_name")
	private String application_name;
	@XmlElement(name="account_name")
	private String account_name;
	@XmlElement(name="severity")
	private String severity;
	@XmlElement(name="message")
	private String message;
	@XmlElement(name="short_description")
	private String short_description;
	@XmlElement(name="long_description")
	private String long_description;
	@XmlElement(name="alert_url")
	private URL alert_url;
	public String getCreated_at() {
		return created_at;
	}
	public void setCreated_at(String created_at) {
		this.created_at = created_at;
	}
	public String getApplication_name() {
		return application_name;
	}
	public void setApplication_name(String application_name) {
		this.application_name = application_name;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
	public String getSeverity() {
		return severity;
	}
	public void setSeverity(String severity) {
		this.severity = severity;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public String getShort_description() {
		return short_description;
	}
	public void setShort_description(String short_description) {
		this.short_description = short_description;
	}
	public String getLong_description() {
		return long_description;
	}
	public void setLong_description(String long_description) {
		this.long_description = long_description;
	}
	public URL getAlert_url() {
		return alert_url;
	}
	public void setAlert_url(URL alert_url) {
		this.alert_url = alert_url;
	}
}

and one for deployments:

package eu.robusta.newrelic.object;

import java.net.URL;

public class Deployment {
	/*
	 * deployment: {
	 * "created_at":"2013-08-28T21:07:38+00:00",
	 * "application_name": "Application name"
	 * ,"account_name":"Account name",
	 * "changelog":"Changelog for deployment",
	 * "description":"Information about deployment",
	 * "revision":"Revision number",
	 * "deployment_url":"https://rpm.newrelic.com/accounts/[account_id]/applications/[application_id]/deployments/[deployment_id]"
	 * ,"deployed_by":"Name of person deploying"
	 * }
	 */
	private String created_at;
	private String application_name;
	private String account_name;
	private String changelog;
	private String description;
	private String revision;
	private URL deployment_url;
	private String deployed_by;
	public String getCreated_at() {
		return created_at;
	}
	public void setCreated_at(String created_at) {
		this.created_at = created_at;
	}
	public String getApplication_name() {
		return application_name;
	}
	public void setApplication_name(String application_name) {
		this.application_name = application_name;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
	public String getChangelog() {
		return changelog;
	}
	public void setChangelog(String changelog) {
		this.changelog = changelog;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public String getRevision() {
		return revision;
	}
	public void setRevision(String revision) {
		this.revision = revision;
	}
	public URL getDeployment_url() {
		return deployment_url;
	}
	public void setDeployment_url(URL deployment_url) {
		this.deployment_url = deployment_url;
	}
	public String getDeployed_by() {
		return deployed_by;
	}
	public void setDeployed_by(String deployed_by) {
		this.deployed_by = deployed_by;
	}
}

Converting a simple JSON to a java class is plain simple when using Jackson. Our conversion looks like:

package eu.robusta.utils;

import java.io.IOException;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

import eu.robusta.newrelic.object.Alert;
import eu.robusta.newrelic.object.Deployment;

public class JSONConverter {

    public static Alert convertAlert(String json) throws JsonParseException
                                               , JsonMappingException, IOException{
            Alert alert = new ObjectMapper().readValue(json, Alert.class);
          
            return alert;
    }
    
    public static Deployment convertDeploy(String json) throws JsonParseException, JsonMappingException, IOException {
    	Deployment deploy = new ObjectMapper().readValue(json, Deployment.class);
    	return deploy;
    }
}

Converting the POJO's to a text message, isn't all that hard either. I just got the most important parts from the original message, concatenated them, and then truncate them to fit in one text message. Here's the code I came up with:

package eu.robusta.utils;

import eu.robusta.newrelic.object.Alert;
import eu.robusta.newrelic.object.Deployment;

public class SMSConverter {
    private static int MAX_LENGTH=159;
    public static String convertAlert(Alert alert) {
    	StringBuffer sb = new StringBuffer(alert.getMessage());
    	sb.append(": ");
    	sb.append(alert.getLong_description());
    	sb.append(". ");
    	sb.append(alert.getAlert_url());
    	return sb.toString().substring(0, MAX_LENGTH);
    }
    
    public static String convertDeploy(Deployment deployment) {
    	StringBuffer sb = new StringBuffer(deployment.getApplication_name());
    	sb.append(" deployed by ");
    	sb.append(deployment.getDeployed_by());
    	sb.append(", revision ");
    	sb.append(deployment.getRevision());
    	sb.append(".");
    	sb.append(deployment.getDeployment_url());
    	return sb.toString().substring(0, MAX_LENGTH);
    }
}

So, this is the end of my post on this experiment. I hope you can use this in your environment, and would love to hear your feedback! Together we can make this an awesome tool!

Reactie toevoegen