package com.intrbiz.bergamot.notification.engine.slack;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.SecureRandom;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import org.apache.log4j.Logger;
import com.intrbiz.Util;
import com.intrbiz.accounting.Accounting;
import com.intrbiz.bergamot.accounting.model.SendNotificationToContactAccountingEvent;
import com.intrbiz.bergamot.crypto.util.BergamotTrustManager;
import com.intrbiz.bergamot.model.message.notification.CheckNotification;
import com.intrbiz.bergamot.model.message.notification.Notification;
import com.intrbiz.bergamot.notification.AbstractNotificationEngine;
import com.intrbiz.bergamot.notification.NotificationException;
import com.intrbiz.bergamot.notification.engine.slack.express.SlackEncode;
import com.intrbiz.bergamot.notification.engine.slack.io.SlackClientHandler;
import com.intrbiz.bergamot.notification.engine.slack.model.SlackMessage;
import com.intrbiz.express.ExpressContext;
import com.intrbiz.express.template.ExpressTemplate;
import com.intrbiz.util.IBThreadFactory;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslHandler;
public class SlackEngine extends AbstractNotificationEngine
{
public static final String NAME = "slack";
private Logger logger = Logger.getLogger(SlackEngine.class);
private SSLContext sslContext;
private EventLoopGroup eventLoop;
private final Timer timer;
private Accounting accounting = Accounting.create(SlackEngine.class);
public SlackEngine()
{
super(NAME);
// timer used for timeouts
this.timer = new Timer();
// every 5 minutes forcefully purge the timer queue
// we cancel most task (hopefully)
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run()
{
// purge the timer queue
timer.purge();
}
}, 300_000L, 300_000L);
}
@Override
protected void configure() throws Exception
{
super.configure();
// setup our custom functions for templating
this.expressExtensions.addFunction("slack_encode", SlackEncode.class);
// setup our HTTP engine
// Setup a custom SSLContext which will not validate certs
this.sslContext = SSLContext.getInstance("TLS");
this.sslContext.init(null, new TrustManager[] { new BergamotTrustManager(false) }, new SecureRandom());
// setup the Netty event loop
this.eventLoop = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors() + 2, new IBThreadFactory("bergamot-slack-notifier", false));
// log
logger.info("Slack notifier configured");
}
public void shutdown()
{
try
{
this.eventLoop.shutdownGracefully().await();
}
catch (InterruptedException e)
{
}
}
protected void buildMessage(CheckNotification notification, SlackMessage message) throws Exception
{
String templateName = notification.getCheck().getCheckType() + "." + notification.getNotificationType();
ExpressContext context = this.createContext(notification);
// add the slack message to the context
context.setEntity("slack_message", message, null);
// load the template
ExpressTemplate template = this.templateLoader.load(context, templateName);
if (template == null) throw new NotificationException("Failed to find template: " + templateName);
// process the template
message.rawText(template.encodeToString(context, notification));
if (logger.isTraceEnabled())
logger.trace(message.toString());
}
@Override
public void sendNotification(Notification notification)
{
// we only handle check notifications
if (notification instanceof CheckNotification)
{
// the slack URL is stored as a parameter on either: contacts, teams, hosts or services
for (String slackUrl : this.findNotificationParameters((CheckNotification) notification, "slack.url"))
{
try
{
// parse the URL
URL url = new URL(slackUrl);
// build the message to send
SlackMessage slackMessage = new SlackMessage()
.username("Bergamot Monitoring")
.iconUrl("https://github.com/intrbiz/bergamot-site/raw/master/src/main/public/logo/disk/64/bergamot_disk_64.png");
// optionally set the channel
if (! Util.isEmpty(url.getRef()))
{
if (logger.isDebugEnabled()) logger.debug("Sending slack message to channel: " + url.getRef());
slackMessage.channel(url.getRef());
}
// template the message
this.buildMessage((CheckNotification) notification, slackMessage);
// build the request to send
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, url.getPath(), Unpooled.wrappedBuffer(slackMessage.toBytes()));
request.headers().add(HttpHeaders.Names.HOST, url.getPort() == -1 ? url.getHost() : url.getHost() + ":" + url.getPort());
request.headers().add(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
request.headers().add(HttpHeaders.Names.USER_AGENT, "Bergamot Monitoring Slack Notifier 1.0.0");
request.headers().add(HttpHeaders.Names.CONTENT_TYPE, "application/json; charset=utf-8");
request.headers().add(HttpHeaders.Names.CONTENT_LENGTH, request.content().readableBytes());
// make a HTTP request
Bootstrap b = new Bootstrap();
b.group(this.eventLoop);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) TimeUnit.SECONDS.toMillis(60));
b.handler(new ChannelInitializer<SocketChannel>()
{
@Override
public void initChannel(SocketChannel ch) throws Exception
{
// Force SSL handling
SSLEngine sslEngine = null;
sslEngine = sslContext.createSSLEngine(url.getHost(), url.getPort() == -1 ? 443 : url.getPort());
sslEngine.setUseClientMode(true);
SSLParameters params = new SSLParameters();
params.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(params);
ch.pipeline().addLast(new SslHandler(sslEngine));
// HTTP handling
ch.pipeline().addLast(
new HttpClientCodec(),
new HttpContentDecompressor(),
new HttpObjectAggregator(1 * 1024 * 1024 /* 1 MiB */),
new SlackClientHandler(timer, sslEngine, request)
);
}
});
// connect the client
b.connect(url.getHost(), url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
// accounting
this.accounting.account(new SendNotificationToContactAccountingEvent(
notification.getSite().getId(),
notification.getId(),
getObjectId(notification),
getNotificationType(notification),
null,
this.getName(),
"slack",
url.toString(),
null
));
}
catch (MalformedURLException e)
{
logger.error("Cannot send Slack notification to malformed url", e);
}
catch (Exception e)
{
logger.error("Failed to send Slack notification", e);
}
}
}
}
}