1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.log; 18 19 import com.android.tradefed.config.Option; 20 import com.android.tradefed.config.Option.Importance; 21 import com.android.tradefed.config.OptionClass; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.util.Email; 24 import com.android.tradefed.util.IEmail; 25 import com.android.tradefed.util.IEmail.Message; 26 27 import java.io.IOException; 28 import java.io.PrintWriter; 29 import java.io.StringWriter; 30 import java.net.InetAddress; 31 import java.net.UnknownHostException; 32 import java.util.Collection; 33 import java.util.HashSet; 34 import java.util.Iterator; 35 36 /** 37 * A simple handler class that sends an email to interested people when a WTF 38 * (What a Terrible Failure) error occurs within a Trade Federation instance. 39 */ 40 @OptionClass(alias = "wtf-email-handler") 41 public class TerribleFailureEmailHandler implements ITerribleFailureHandler { 42 private static final String DEFAULT_SUBJECT_PREFIX = "WTF happened to tradefed"; 43 44 @Option(name = "sender", 45 description = "The envelope-sender address to use for the messages.", 46 importance = Importance.IF_UNSET) 47 private String mSender = null; 48 49 @Option(name = "destination", 50 description = "One or more destination addresses.", 51 importance = Importance.IF_UNSET) 52 private Collection<String> mDestinations = new HashSet<String>(); 53 54 @Option(name = "subject-prefix", 55 description = "The prefix to be added to the beginning of the email subject.") 56 private String mSubjectPrefix = DEFAULT_SUBJECT_PREFIX; 57 58 @Option(name = "min-email-interval", 59 description = "The minimum interval between emails in ms. " + 60 "If a new WTF happens within this interval from the previous one, " + 61 "it will be ignored.") 62 private long mMinEmailInterval = 5 * 60 * 1000; 63 64 private IEmail mMailer; 65 private long mLastEmailSentTime = 0; 66 67 /** 68 * Create a {@link TerribleFailureEmailHandler} 69 */ TerribleFailureEmailHandler()70 public TerribleFailureEmailHandler() { 71 this(new Email()); 72 } 73 74 /** 75 * Create a {@link TerribleFailureEmailHandler} with a custom {@link IEmail} 76 * instance to use. 77 * <p/> 78 * Exposed for unit testing. 79 * 80 * @param mailer the {@link IEmail} instance to use. 81 */ TerribleFailureEmailHandler(IEmail mailer)82 protected TerribleFailureEmailHandler(IEmail mailer) { 83 mMailer = mailer; 84 } 85 86 /** 87 * Adds an email destination address. 88 * 89 * @param dest 90 */ addDestination(String dest)91 public void addDestination(String dest) { 92 mDestinations.add(dest); 93 } 94 95 /** 96 * Sets the email sender address. 97 * 98 * @param sender 99 */ setSender(String sender)100 public void setSender(String sender) { 101 mSender = sender; 102 } 103 104 /** 105 * Sets the minimum email interval. 106 * 107 * @param interval 108 */ setMinEmailInterval(long interval)109 public void setMinEmailInterval(long interval) { 110 mMinEmailInterval = interval; 111 } 112 113 /** 114 * Gets the local host name of the machine. 115 * 116 * @return the name of the host machine, or "unknown host" if unknown 117 */ getLocalHostName()118 protected String getLocalHostName() { 119 try { 120 return InetAddress.getLocalHost().getHostName(); 121 } catch (UnknownHostException e) { 122 CLog.e(e); 123 return "unknown host"; 124 } 125 } 126 127 /** 128 * Gets the current time in milliseconds. 129 */ getCurrentTimeMillis()130 protected long getCurrentTimeMillis() { 131 return System.currentTimeMillis(); 132 } 133 134 /** 135 * A method to generate the subject for email reports. 136 * The subject will be formatted as: 137 * "<subject-prefix> on <local-host-name>" 138 * 139 * @return A {@link String} containing the subject to use for an email report 140 */ generateEmailSubject()141 protected String generateEmailSubject() { 142 final StringBuilder subj = new StringBuilder(); 143 144 subj.append(mSubjectPrefix); 145 subj.append(" on "); 146 subj.append(getLocalHostName()); 147 148 return subj.toString(); 149 } 150 151 /** 152 * A method to generate the body for WTF email reports. 153 * 154 * @param message summary of the terrible failure 155 * @param cause throwable containing stack trace information 156 * @return A {@link String} containing the body to use for an email report 157 */ generateEmailBody(String message, Throwable cause)158 protected String generateEmailBody(String message, Throwable cause) { 159 StringBuilder bodyBuilder = new StringBuilder(); 160 161 bodyBuilder.append("host: "); 162 bodyBuilder.append(getLocalHostName()); 163 bodyBuilder.append("\n\n"); 164 165 bodyBuilder.append("What a Terrible Failure! "); 166 bodyBuilder.append(message); 167 bodyBuilder.append("\n\n"); 168 169 if (cause != null) { 170 bodyBuilder.append("Invocation failed: "); 171 bodyBuilder.append(getStackTraceString(cause)); 172 bodyBuilder.append("\n\n"); 173 } 174 175 return bodyBuilder.toString(); 176 } 177 178 /** 179 * Generates a new email message based on the attributes already gathered 180 * (subject, sender, destinations), as well as the description and cause (Optional) 181 * 182 * @param description Summary of the terrible failure 183 * @param cause (Optional) Throwable that includes stack trace info 184 * @return Message object with all email attributes populated 185 */ generateEmailMessage(String description, Throwable cause)186 protected Message generateEmailMessage(String description, Throwable cause) { 187 Message msg = new Message(); 188 msg.setSender(mSender); 189 msg.setSubject(generateEmailSubject()); 190 msg.setBody(generateEmailBody(description, cause)); 191 msg.setHtml(false); 192 Iterator<String> toAddress = mDestinations.iterator(); 193 while (toAddress.hasNext()) { 194 msg.addTo(toAddress.next()); 195 } 196 return msg; 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 @Override onTerribleFailure(String description, Throwable cause)203 public boolean onTerribleFailure(String description, Throwable cause) { 204 if (mDestinations.isEmpty()) { 205 CLog.e("Failed to send email because no destination addresses were set."); 206 return false; 207 } 208 209 final long now = getCurrentTimeMillis(); 210 if (0 < mMinEmailInterval && now - mLastEmailSentTime < mMinEmailInterval) { 211 // TODO: consider queuing up skipped failures and send it later. 212 CLog.w("Skipped to send %s email: email interval %dms < %dms", DEFAULT_SUBJECT_PREFIX, 213 now - mLastEmailSentTime, mMinEmailInterval); 214 return false; 215 } 216 217 Message msg = generateEmailMessage(description, cause); 218 219 try { 220 mMailer.send(msg); 221 } catch (IllegalArgumentException e) { 222 CLog.e("Failed to send %s email", DEFAULT_SUBJECT_PREFIX); 223 CLog.e(e); 224 return false; 225 } catch (IOException e) { 226 CLog.e("Failed to send %s email", DEFAULT_SUBJECT_PREFIX); 227 CLog.e(e); 228 return false; 229 } 230 231 mLastEmailSentTime = now; 232 return true; 233 } 234 235 /** 236 * A helper method that parses the stack trace string out of the throwable. 237 * 238 * @param t contains the stack trace information 239 * @return A {@link String} containing the stack trace of the throwable. 240 */ getStackTraceString(Throwable t)241 private static String getStackTraceString(Throwable t) { 242 if (t == null) 243 return ""; 244 245 StringWriter sw = new StringWriter(); 246 PrintWriter pw = new PrintWriter(sw); 247 t.printStackTrace(pw); 248 pw.flush(); 249 return sw.toString(); 250 } 251 } 252