package com.intrbiz.bergamot.notification.engine.sms; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.stream.Collectors; import org.apache.log4j.Logger; import com.codahale.metrics.Counter; import com.codahale.metrics.Timer; import com.intrbiz.Util; import com.intrbiz.accounting.Accounting; import com.intrbiz.bergamot.accounting.model.SendNotificationToContactAccountingEvent; import com.intrbiz.bergamot.health.HealthTracker; import com.intrbiz.bergamot.health.model.KnownDaemon; import com.intrbiz.bergamot.model.message.ContactMO; 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.engine.sms.io.AWSTransport; import com.intrbiz.bergamot.notification.engine.sms.io.SMSTransport; import com.intrbiz.bergamot.notification.engine.sms.io.SMSTransportException; import com.intrbiz.bergamot.notification.engine.sms.io.TwilioTransport; import com.intrbiz.bergamot.notification.engine.sms.model.SMSMessage; import com.intrbiz.bergamot.notification.engine.sms.model.SentSMS; import com.intrbiz.configuration.CfgParameter; import com.intrbiz.gerald.source.IntelligenceSource; import com.intrbiz.gerald.witchcraft.Witchcraft; import com.intrbiz.queue.QueueException; public class SMSEngine extends AbstractNotificationEngine { public static final String NAME = "sms"; private static final Logger logger = Logger.getLogger(SMSEngine.class); private String from; private final Timer smsSendTimer; private final Counter smsSendErrors; private Accounting accounting = Accounting.create(SMSEngine.class); private List<String> healthcheckAdmins = new LinkedList<String>(); private SMSTransport transport; public SMSEngine() { super(NAME); // setup metrics IntelligenceSource source = Witchcraft.get().source("com.intrbiz.bergamot.sms"); this.smsSendTimer = source.getRegistry().timer("sms-sent"); this.smsSendErrors = source.getRegistry().counter("sms-errors"); } protected SMSTransport loadTransport(String transportName) throws SMSTransportException { switch (transportName) { case "twilio": return new TwilioTransport(); case "aws": return new AWSTransport(); } throw new SMSTransportException("The transport: " + transportName + " is not known, cannot load transport"); } @Override protected void configure() throws Exception { super.configure(); // from number this.from = this.config.getStringParameterValue("from", ""); logger.info("Sending SMS messages from: " + this.from); // load our SMS transport this.transport = this.loadTransport(this.config.getStringParameterValue("transport", "twilio")); this.transport.configure(this.config); logger.info("Using " + this.transport + " to send SMS messages"); // who to contact in the event we get a warning from the healthcheck subsystem for (CfgParameter param : this.config.getParameters()) { if ("healthcheck.admin".equals(param.getName()) && (! Util.isEmpty(param.getValueOrText()))) this.healthcheckAdmins.add(param.getValueOrText()); } logger.info("Healthcheck alerts will be sent to " + this.healthcheckAdmins); // setup healthchecking HealthTracker.getInstance().addAlertHandler(this::raiseHealthcheckAlert); } public void raiseHealthcheckAlert(KnownDaemon failed) { logger.error("Got healthcheck alert for " + failed.getDaemonName() + " [" + failed.getInstanceId() + "] on host " + failed.getHostName() + " [" + failed.getHostId() + "]"); if (! this.healthcheckAdmins.isEmpty()) { // really try to send for (int i = 0; i < 10; i++) { try { String message = this.buildMessage(failed); // send the SMSes for (String admin : this.healthcheckAdmins) { try { this.transport.sendSMS(new SMSMessage(admin, this.from, message)); } catch (SMSTransportException e) { logger.error("Failed to send healthcheck SMS to " + admin, e); } } // successfully sent break; } catch (Exception e) { logger.error("Failed to send SMS healthcheck notification", e); } } } } @Override public void sendNotification(Notification notification) { logger.info("Sending SMS notification for " + notification.getNotificationType() + " to " + notification.getTo().stream().map(ContactMO::getPager).filter((e) -> { return e != null; }).collect(Collectors.toList())); Timer.Context tctx = this.smsSendTimer.time(); try { try { Map<String, ContactMO> tos = this.getTo(notification); if (tos.isEmpty()) return; // build the message String message = this.buildMessage(notification); if (Util.isEmpty(message)) throw new RuntimeException("Failed to build message, not sending notifications"); // send the SMSes for (Entry<String, ContactMO> to : tos.entrySet()) { try { SMSMessage sms = new SMSMessage(to.getKey(), this.from, message); if (logger.isDebugEnabled()) logger.debug("Sending SMS: " + sms); SentSMS sent = this.transport.sendSMS(sms); if (logger.isDebugEnabled()) logger.debug("Sent SMS: " + sent); // accounting this.accounting.account(new SendNotificationToContactAccountingEvent( notification.getSite().getId(), notification.getId(), getObjectId(notification), getNotificationType(notification), to.getValue().getId(), this.getName(), "sms", sms.getTo(), sent.getMessageId() )); } catch (Exception e) { this.smsSendErrors.inc(); logger.error("Failed to send SMS notification to " + to.getKey() + " - " + to.getValue().getName(), e); } } } catch (Exception e) { this.smsSendErrors.inc(); logger.error("Failed to send SMS notification", e); throw new QueueException("Failed to send email notification", e); } } finally { tctx.stop(); } } protected Map<String, ContactMO> getTo(Notification notification) { Map<String, ContactMO> to = new TreeMap<String, ContactMO>(); for (ContactMO contact : notification.getTo()) { if ((!Util.isEmpty(Util.coalesceEmpty(contact.getPager(), contact.getMobile()))) && contact.hasEngine(this.getName())) { to.put(Util.coalesceEmpty(contact.getPager(), contact.getMobile()), contact); } } return to; } protected String buildMessage(Notification notification) throws Exception { if (notification instanceof CheckNotification) { return this.applyTemplate(((CheckNotification)notification).getCheck().getCheckType() + "." + notification.getNotificationType() + ".message", notification); } else { return this.applyTemplate(notification.getNotificationType() + ".message", notification); } } protected String buildMessage(KnownDaemon daemon) throws Exception { return this.applyTemplate("healthcheck.alert.message", daemon); } }