001/** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 019 020package org.apache.oozie.sla.listener; 021 022import java.util.ArrayList; 023import java.util.HashSet; 024import java.util.List; 025import java.util.Properties; 026import java.util.Set; 027import java.util.concurrent.TimeUnit; 028import java.util.concurrent.atomic.AtomicInteger; 029 030import javax.mail.Address; 031import javax.mail.Message; 032import javax.mail.MessagingException; 033import javax.mail.NoSuchProviderException; 034import javax.mail.SendFailedException; 035import javax.mail.Session; 036import javax.mail.Transport; 037import javax.mail.internet.AddressException; 038import javax.mail.internet.InternetAddress; 039import javax.mail.internet.MimeMessage; 040import javax.mail.internet.MimeMessage.RecipientType; 041 042import org.apache.hadoop.conf.Configuration; 043import org.apache.oozie.action.email.EmailActionExecutor; 044import org.apache.oozie.action.email.EmailActionExecutor.JavaMailAuthenticator; 045import org.apache.oozie.client.event.SLAEvent; 046import org.apache.oozie.service.ConfigurationService; 047import org.apache.oozie.sla.listener.SLAEventListener; 048import org.apache.oozie.sla.service.SLAService; 049import org.apache.oozie.util.XLog; 050 051import com.google.common.annotations.VisibleForTesting; 052import com.google.common.cache.CacheBuilder; 053import com.google.common.cache.CacheLoader; 054import com.google.common.cache.LoadingCache; 055 056public class SLAEmailEventListener extends SLAEventListener { 057 058 public static final String SMTP_CONNECTION_TIMEOUT = EmailActionExecutor.CONF_PREFIX + "smtp.connectiontimeout"; 059 public static final String SMTP_TIMEOUT = EmailActionExecutor.CONF_PREFIX + "smtp.timeout"; 060 public static final String BLACKLIST_CACHE_TIMEOUT = EmailActionExecutor.CONF_PREFIX + "blacklist.cachetimeout"; 061 public static final String BLACKLIST_FAIL_COUNT = EmailActionExecutor.CONF_PREFIX + "blacklist.failcount"; 062 public static final String OOZIE_BASE_URL = "oozie.base.url"; 063 private Session session; 064 private String oozieBaseUrl; 065 private InternetAddress fromAddr; 066 private String ADDRESS_SEPARATOR = ","; 067 private LoadingCache<String, AtomicInteger> blackList; 068 private int blacklistFailCount; 069 private final String BLACKLIST_CACHE_TIMEOUT_DEFAULT = "1800"; // in sec. default to 30 min 070 private final String BLACKLIST_FAIL_COUNT_DEFAULT = "2"; // stop sending when fail count equals or exceeds 071 private final String SMTP_HOST_DEFAULT = "localhost"; 072 private final String SMTP_PORT_DEFAULT = "25"; 073 private final boolean SMTP_AUTH_DEFAULT = false; 074 private final String SMTP_SOURCE_DEFAULT = "oozie@localhost"; 075 private final String SMTP_CONNECTION_TIMEOUT_DEFAULT = "5000"; 076 private final String SMTP_TIMEOUT_DEFAULT = "5000"; 077 private static XLog LOG = XLog.getLog(SLAEmailEventListener.class); 078 private Set<SLAEvent.EventStatus> alertEvents; 079 public static String EMAIL_BODY_FIELD_SEPARATER = " - "; 080 public static String EMAIL_BODY_FIELD_INDENT = " "; 081 public static String EMAIL_BODY_HEADER_SEPARATER = ":"; 082 083 public enum EmailField { 084 EVENT_STATUS("SLA Status"), APP_TYPE("App Type"), APP_NAME("App Name"), USER("User"), JOBID("Job ID"), PARENT_JOBID( 085 "Parent Job ID"), JOB_URL("Job URL"), PARENT_JOB_URL("Parent Job URL"), NOMINAL_TIME("Nominal Time"), 086 EXPECTED_START_TIME("Expected Start Time"), ACTUAL_START_TIME("Actual Start Time"), 087 EXPECTED_END_TIME("Expected End Time"), ACTUAL_END_TIME("Actual End Time"), 088 EXPECTED_DURATION("Expected Duration (in mins)"), 089 ACTUAL_DURATION("Actual Duration (in mins)"), NOTIFICATION_MESSAGE("Notification Message"), 090 UPSTREAM_APPS("Upstream Apps"), 091 JOB_STATUS("Job Status"); 092 private String name; 093 094 private EmailField(String name) { 095 this.name = name; 096 } 097 098 @Override 099 public String toString() { 100 return name; 101 } 102 }; 103 104 @Override 105 public void init(Configuration conf) throws Exception { 106 107 oozieBaseUrl = ConfigurationService.get(conf, OOZIE_BASE_URL); 108 // Get SMTP properties from the configuration used in Email Action 109 String smtpHost = conf.get(EmailActionExecutor.EMAIL_SMTP_HOST, SMTP_HOST_DEFAULT); 110 String smtpPort = conf.get(EmailActionExecutor.EMAIL_SMTP_PORT, SMTP_PORT_DEFAULT); 111 Boolean smtpAuth = conf.getBoolean(EmailActionExecutor.EMAIL_SMTP_AUTH, SMTP_AUTH_DEFAULT); 112 String smtpUser = conf.get(EmailActionExecutor.EMAIL_SMTP_USER, ""); 113 String smtpPassword = ConfigurationService.getPassword(EmailActionExecutor.EMAIL_SMTP_PASS, ""); 114 String smtpConnectTimeout = conf.get(SMTP_CONNECTION_TIMEOUT, SMTP_CONNECTION_TIMEOUT_DEFAULT); 115 String smtpTimeout = conf.get(SMTP_TIMEOUT, SMTP_TIMEOUT_DEFAULT); 116 117 int blacklistTimeOut = Integer.valueOf(conf.get(BLACKLIST_CACHE_TIMEOUT, BLACKLIST_CACHE_TIMEOUT_DEFAULT)); 118 blacklistFailCount = Integer.valueOf(conf.get(BLACKLIST_FAIL_COUNT, BLACKLIST_FAIL_COUNT_DEFAULT)); 119 120 // blacklist email addresses causing SendFailedException with cache timeout 121 blackList = CacheBuilder.newBuilder() 122 .expireAfterWrite(blacklistTimeOut, TimeUnit.SECONDS) 123 .build(new CacheLoader<String, AtomicInteger>() { 124 @Override 125 public AtomicInteger load(String key) throws Exception { 126 return new AtomicInteger(); 127 } 128 }); 129 130 // Set SMTP properties 131 Properties properties = new Properties(); 132 properties.setProperty("mail.smtp.host", smtpHost); 133 properties.setProperty("mail.smtp.port", smtpPort); 134 properties.setProperty("mail.smtp.auth", smtpAuth.toString()); 135 properties.setProperty("mail.smtp.connectiontimeout", smtpConnectTimeout); 136 properties.setProperty("mail.smtp.timeout", smtpTimeout); 137 138 try { 139 fromAddr = new InternetAddress(conf.get("oozie.email.from.address", SMTP_SOURCE_DEFAULT)); 140 } 141 catch (AddressException ae) { 142 LOG.error("Bad Source Address specified in oozie.email.from.address", ae); 143 throw ae; 144 } 145 146 if (!smtpAuth) { 147 session = Session.getInstance(properties); 148 } 149 else { 150 session = Session.getInstance(properties, new JavaMailAuthenticator(smtpUser, smtpPassword)); 151 } 152 153 alertEvents = new HashSet<SLAEvent.EventStatus>(); 154 String alertEventsStr = ConfigurationService.get(conf, SLAService.CONF_ALERT_EVENTS); 155 if (alertEventsStr != null) { 156 String[] alertEvt = alertEventsStr.split(",", -1); 157 for (String evt : alertEvt) { 158 alertEvents.add(SLAEvent.EventStatus.valueOf(evt)); 159 } 160 } 161 } 162 163 @Override 164 public void destroy() { 165 } 166 167 private void sendSLAEmail(SLAEvent event) throws Exception { 168 // If no address is provided, the user did not want to send an email so simply log it and do nothing 169 if (event.getAlertContact() == null || event.getAlertContact().trim().length() == 0) { 170 LOG.info("No destination address provided; an SLA alert email will not be sent"); 171 } else { 172 // Create and send an email 173 Message message = new MimeMessage(session); 174 setMessageHeader(message, event); 175 setMessageBody(message, event); 176 sendEmail(message); 177 } 178 } 179 180 @Override 181 public void onStartMiss(SLAEvent event) { 182 boolean flag = false; 183 if (event.getAlertEvents() == null) { 184 flag = alertEvents.contains(SLAEvent.EventStatus.START_MISS); 185 } 186 else if (event.getAlertEvents().contains(SLAEvent.EventStatus.START_MISS.name())) { 187 flag = true; 188 } 189 190 if (flag) { 191 try { 192 sendSLAEmail(event); 193 } 194 catch (Exception e) { 195 LOG.error("Failed to send StartMiss alert email", e); 196 } 197 } 198 } 199 200 @Override 201 public void onEndMiss(SLAEvent event) { 202 boolean flag = false; 203 if (event.getAlertEvents() == null) { 204 flag = alertEvents.contains(SLAEvent.EventStatus.END_MISS); 205 } 206 else if (event.getAlertEvents().contains(SLAEvent.EventStatus.END_MISS.name())) { 207 flag = true; 208 } 209 210 if (flag) { 211 try { 212 sendSLAEmail(event); 213 } 214 catch (Exception e) { 215 LOG.error("Failed to send EndMiss alert email", e); 216 } 217 } 218 } 219 220 @Override 221 public void onDurationMiss(SLAEvent event) { 222 boolean flag = false; 223 if (event.getAlertEvents() == null) { 224 flag = alertEvents.contains(SLAEvent.EventStatus.DURATION_MISS); 225 } 226 else if (event.getAlertEvents().contains(SLAEvent.EventStatus.DURATION_MISS.name())) { 227 flag = true; 228 } 229 230 if (flag) { 231 try { 232 sendSLAEmail(event); 233 } 234 catch (Exception e) { 235 LOG.error("Failed to send DurationMiss alert email", e); 236 } 237 } 238 } 239 240 private Address[] parseAddress(String str) { 241 Address[] addrs = null; 242 List<InternetAddress> addrList = new ArrayList<InternetAddress>(); 243 String[] emails = str.split(ADDRESS_SEPARATOR, -1); 244 245 for (String email : emails) { 246 boolean isBlackListed = false; 247 AtomicInteger val = blackList.getIfPresent(email); 248 if(val != null){ 249 isBlackListed = ( val.get() >= blacklistFailCount ); 250 } 251 if (!isBlackListed) { 252 try { 253 // turn on strict syntax check by setting 2nd argument true 254 addrList.add(new InternetAddress(email, true)); 255 } 256 catch (AddressException ae) { 257 // simply skip bad address but do not throw exception 258 LOG.error("Skipping bad destination address: " + email, ae); 259 } 260 } 261 } 262 263 if (addrList.size() > 0) { 264 addrs = (Address[]) addrList.toArray(new InternetAddress[addrList.size()]); 265 } 266 267 return addrs; 268 } 269 270 private void setMessageHeader(Message msg, SLAEvent event) throws MessagingException { 271 Address[] from = new InternetAddress[] { fromAddr }; 272 Address[] to; 273 StringBuilder subject = new StringBuilder(); 274 275 to = parseAddress(event.getAlertContact()); 276 if (to == null) { 277 LOG.error("Destination address is null or invalid, stop sending SLA alert email"); 278 throw new IllegalArgumentException("Destination address is not specified properly"); 279 } 280 subject.append("OOZIE - SLA "); 281 subject.append(event.getEventStatus().name()); 282 subject.append(" (AppName="); 283 subject.append(event.getAppName()); 284 subject.append(", JobID="); 285 subject.append(event.getId()); 286 subject.append(")"); 287 288 try { 289 msg.addFrom(from); 290 msg.addRecipients(RecipientType.TO, to); 291 msg.setSubject(subject.toString()); 292 } 293 catch (MessagingException me) { 294 LOG.error("Message Exception in setting message header of SLA alert email", me); 295 throw me; 296 } 297 } 298 299 private void setMessageBody(Message msg, SLAEvent event) throws MessagingException { 300 StringBuilder body = new StringBuilder(); 301 printHeading(body, "Status"); 302 printField(body, EmailField.EVENT_STATUS.toString(), event.getEventStatus()); 303 printField(body, EmailField.JOB_STATUS.toString(), event.getJobStatus()); 304 printField(body, EmailField.NOTIFICATION_MESSAGE.toString(), event.getNotificationMsg()); 305 306 printHeading(body, "Job Details"); 307 printField(body, EmailField.APP_NAME.toString(), event.getAppName()); 308 printField(body, EmailField.APP_TYPE.toString(), event.getAppType()); 309 printField(body, EmailField.USER.toString(), event.getUser()); 310 printField(body, EmailField.JOBID.toString(), event.getId()); 311 printField(body, EmailField.JOB_URL.toString(), getJobLink(event.getId())); 312 printField(body, EmailField.PARENT_JOBID.toString(), event.getParentId() != null ? event.getParentId() : "N/A"); 313 printField(body, EmailField.PARENT_JOB_URL.toString(), 314 event.getParentId() != null ? getJobLink(event.getParentId()) : "N/A"); 315 printField(body, EmailField.UPSTREAM_APPS.toString(), event.getUpstreamApps()); 316 317 printHeading(body, "SLA Details"); 318 printField(body, EmailField.NOMINAL_TIME.toString(), event.getNominalTime()); 319 printField(body, EmailField.EXPECTED_START_TIME.toString(), event.getExpectedStart()); 320 printField(body, EmailField.ACTUAL_START_TIME.toString(), event.getActualStart()); 321 printField(body, EmailField.EXPECTED_END_TIME.toString(), event.getExpectedEnd()); 322 printField(body, EmailField.ACTUAL_END_TIME.toString(), event.getActualEnd()); 323 printField(body, EmailField.EXPECTED_DURATION.toString(), getDurationInMins(event.getExpectedDuration())); 324 printField(body, EmailField.ACTUAL_DURATION.toString(), getDurationInMins(event.getActualDuration())); 325 326 try { 327 msg.setText(body.toString()); 328 } 329 catch (MessagingException me) { 330 LOG.error("Message Exception in setting message body of SLA alert email", me); 331 throw me; 332 } 333 } 334 335 private long getDurationInMins(long duration) { 336 if (duration < 0) { 337 return duration; 338 } 339 return duration / 60000; //Convert millis to minutes 340 } 341 342 private String getJobLink(String jobId) { 343 StringBuffer url = new StringBuffer(); 344 String param = "/?job="; 345 url.append(oozieBaseUrl); 346 url.append(param); 347 url.append(jobId); 348 return url.toString(); 349 } 350 351 private void printField(StringBuilder st, String name, Object value) { 352 String lineFeed = "\n"; 353 if (value != null) { 354 st.append(EMAIL_BODY_FIELD_INDENT); 355 st.append(name); 356 st.append(EMAIL_BODY_FIELD_SEPARATER); 357 st.append(value); 358 st.append(lineFeed); 359 } 360 } 361 362 private void printHeading(StringBuilder st, String header) { 363 st.append(header); 364 st.append(EMAIL_BODY_HEADER_SEPARATER); 365 st.append("\n"); 366 } 367 368 private void sendEmail(Message message) throws MessagingException { 369 try { 370 Transport.send(message); 371 } 372 catch (NoSuchProviderException se) { 373 LOG.error("Could not find an SMTP transport provider to email", se); 374 throw se; 375 } 376 catch (MessagingException me) { 377 LOG.error("Message Exception in transporting SLA alert email", me); 378 if (me instanceof SendFailedException) { 379 Address[] invalidAddrs = ((SendFailedException) me).getInvalidAddresses(); 380 if (invalidAddrs != null && invalidAddrs.length > 0) { 381 for (Address addr : invalidAddrs) { 382 try { 383 // 'get' method loads key into cache when it doesn't exist 384 AtomicInteger val = blackList.get(addr.toString()); 385 val.incrementAndGet(); 386 } 387 catch (Exception e) { 388 LOG.debug("blacklist loading threw exception: " + e.getMessage()); 389 } 390 } 391 } 392 } 393 throw me; 394 } 395 } 396 397 @VisibleForTesting 398 public void addBlackList(String email) throws Exception { 399 // this is for testing 400 if(email == null || email.equals("")){ 401 return; 402 } 403 AtomicInteger val = blackList.get(email); 404 val.set(blacklistFailCount); 405 } 406 407 @Override 408 public void onStartMet(SLAEvent work) { 409 } 410 411 @Override 412 public void onEndMet(SLAEvent work) { 413 } 414 415 @Override 416 public void onDurationMet(SLAEvent work) { 417 } 418 419}