Bläddra i källkod

AMBARI-8969 - Alerts: Each Alert Occurrence Should Send Out SNMP Trap (jonathanhurley)

Jonathan Hurley 10 år sedan
förälder
incheckning
4aa2090ad4
19 ändrade filer med 567 tillägg och 166 borttagningar
  1. 8 0
      ambari-server/src/main/java/org/apache/ambari/server/notifications/NotificationDispatcher.java
  2. 8 0
      ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
  3. 13 4
      ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/SNMPDispatcher.java
  4. 302 93
      ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
  5. 9 7
      ambari-server/src/main/resources/alert-templates.xml
  6. 2 2
      ambari-server/src/main/resources/common-services/FALCON/0.5.0.2.1/alerts.json
  7. 4 4
      ambari-server/src/main/resources/common-services/HDFS/2.1.0.2.0/alerts.json
  8. 2 2
      ambari-server/src/main/resources/common-services/OOZIE/4.0.0.2.0/alerts.json
  9. 2 2
      ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/alerts.json
  10. 4 4
      ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json
  11. 2 2
      ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/OOZIE/alerts.json
  12. 8 8
      ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/YARN/alerts.json
  13. 4 4
      ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json
  14. 4 4
      ambari-server/src/main/resources/stacks/HDP/1.3.2/services/MAPREDUCE/alerts.json
  15. 2 2
      ambari-server/src/main/resources/stacks/HDP/1.3.2/services/OOZIE/alerts.json
  16. 8 8
      ambari-server/src/main/resources/stacks/HDP/2.0.6/services/YARN/alerts.json
  17. 1 1
      ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java
  18. 8 0
      ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java
  19. 176 19
      ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java

+ 8 - 0
ambari-server/src/main/java/org/apache/ambari/server/notifications/NotificationDispatcher.java

@@ -44,4 +44,12 @@ public interface NotificationDispatcher {
    */
   public void dispatch(Notification notification);
 
+  /**
+   * Gets whether the dispatcher supports sending a digest or summary in a
+   * single {@link Notification}. Some providers may not allow the
+   * {@link Notification} to contain information on more than a single event.
+   *
+   * @return {@code true} if digest is supported, {@code false} otherwise.
+   */
+  public boolean isDigestSupported();
 }

+ 8 - 0
ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java

@@ -150,6 +150,14 @@ public class EmailDispatcher implements NotificationDispatcher {
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean isDigestSupported() {
+    return true;
+  }
+
   /**
    * The {@link EmailAuthenticator} class is used to provide a username and
    * password combination to an SMTP server.

+ 13 - 4
ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/SNMPDispatcher.java

@@ -17,7 +17,10 @@
  */
 package org.apache.ambari.server.notifications.dispatchers;
 
-import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
 import org.apache.ambari.server.notifications.Notification;
 import org.apache.ambari.server.notifications.NotificationDispatcher;
 import org.apache.ambari.server.notifications.Recipient;
@@ -46,9 +49,7 @@ import org.snmp4j.smi.VariableBinding;
 import org.snmp4j.transport.DefaultUdpTransportMapping;
 import org.snmp4j.util.DefaultPDUFactory;
 
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
+import com.google.inject.Singleton;
 
 /**
  * The {@link SNMPDispatcher} class is used to dispatch {@link Notification} via SNMP.
@@ -189,6 +190,14 @@ public class SNMPDispatcher implements NotificationDispatcher {
     }
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean isDigestSupported() {
+    return false;
+  }
+
   /**
    * Possible SNMP security levels
    */

+ 302 - 93
ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java

@@ -25,6 +25,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.lang.reflect.Type;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -128,6 +129,26 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
    */
   private static final String AMBARI_DISPATCH_RECIPIENTS = "ambari.dispatch.recipients";
 
+  /**
+   * The context key for Ambari information to be passed to Velocity.
+   */
+  private static final String VELOCITY_AMBARI_KEY = "ambari";
+
+  /**
+   * The context key for alert summary information to be passed to Velocity.
+   */
+  private static final String VELOCITY_SUMMARY_KEY = "summary";
+
+  /**
+   * The context key for a single alert's information to be passed to Velocity.
+   */
+  private static final String VELOCITY_ALERT_KEY = "alert";
+
+  /**
+   * The context key for dispatch target information to be passed to Velocity.
+   */
+  private static final String VELOCITY_DISPATCH_KEY = "dispatch";
+
   /**
    * Gson used to convert JSON properties to a map.
    */
@@ -173,9 +194,8 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
    * Constructor.
    */
   public AlertNoticeDispatchService() {
-    m_executor = new ThreadPoolExecutor(0, 2, 5L,
-        TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(),
-        new AlertDispatchThreadFactory(),
+    m_executor = new ThreadPoolExecutor(0, 2, 5L, TimeUnit.MINUTES,
+        new LinkedBlockingQueue<Runnable>(), new AlertDispatchThreadFactory(),
         new ThreadPoolExecutor.CallerRunsPolicy());
 
     GsonBuilder gsonBuilder = new GsonBuilder();
@@ -224,8 +244,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
     } catch (Exception exception) {
       LOG.error(
           "Unable to load alert template file {}, outbound notifications will not be formatted",
-          AMBARI_ALERT_TEMPLATES,
-          exception);
+          AMBARI_ALERT_TEMPLATES, exception);
     } finally {
       if (null != inputStream) {
         IOUtils.closeQuietly(inputStream);
@@ -233,7 +252,6 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
     }
   }
 
-
   /**
    * Sets the {@link Executor} to use when dispatching {@link Notification}s.
    * This should only be used by unit tests to provide a mock executor.
@@ -256,8 +274,8 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
       return;
     }
 
-    LOG.info("There are {} pending alert notices about to be dispatched..."
-        + pending.size());
+    LOG.info("There are {} pending alert notices about to be dispatched...",
+        pending.size());
 
     Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap =
         new HashMap<AlertTargetEntity, List<AlertNoticeEntity>>(pending.size());
@@ -284,79 +302,64 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
       }
 
       String targetType = target.getNotificationType();
-      String propertiesJson = target.getProperties();
-
-      AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
-          AlertTargetProperties.class);
-
-      Map<String, String> properties = targetProperties.Properties;
-
-      Notification notification = new Notification();
-      notification.Callback = new AlertNoticeDispatchCallback();
-      notification.CallbackIds = new ArrayList<String>(notices.size());
-
-      List<AlertHistoryEntity> histories = new ArrayList<AlertHistoryEntity>(
-          notices.size());
-
-      // add callback IDs so that the notices can be marked as DELIVERED or
-      // FAILED, and create a list of just the alert histories
-      for (AlertNoticeEntity notice : notices) {
-        AlertHistoryEntity history = notice.getAlertHistory();
-        histories.add(history);
-
-        notification.CallbackIds.add(notice.getUuid());
-      }
+      NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);
 
-      // populate the subject and body fields; if there is a problem
-      // generating the content, then mark the notices as FAILED
-      try {
-        renderNotificationContent(notification, histories, target);
-      } catch (Exception exception) {
-        LOG.error("Unable to create notification for alerts", exception);
+      // create a single digest notification if supported
+      if (dispatcher.isDigestSupported()) {
+        Notification notification = buildNotificationFromTarget(target);
+        notification.CallbackIds = new ArrayList<String>(notices.size());
+        List<AlertHistoryEntity> histories = new ArrayList<AlertHistoryEntity>(
+            notices.size());
 
-        // there was a problem generating content for the target; mark all
-        // notices as FAILED and skip this target
-        List<String> failedNoticeIds = new ArrayList<String>(notices.size());
+        // add callback IDs so that the notices can be marked as DELIVERED or
+        // FAILED, and create a list of just the alert histories
         for (AlertNoticeEntity notice : notices) {
-          failedNoticeIds.add(notice.getUuid());
-        }
-
-        // mark these as failed
-        notification.Callback.onFailure(failedNoticeIds);
-        continue;
-      }
+          AlertHistoryEntity history = notice.getAlertHistory();
+          histories.add(history);
 
-      // set dispatch credentials
-      if (properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_USERNAME)
-          && properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_PASSWORD)) {
-        DispatchCredentials credentials = new DispatchCredentials();
-        credentials.UserName = properties.get(AMBARI_DISPATCH_CREDENTIAL_USERNAME);
-        credentials.Password = properties.get(AMBARI_DISPATCH_CREDENTIAL_PASSWORD);
-        notification.Credentials = credentials;
-      }
-
-      // create recipients
-      if (null != targetProperties.Recipients) {
-        List<Recipient> recipients = new ArrayList<Recipient>(
-            targetProperties.Recipients.size());
-
-        for (String stringRecipient : targetProperties.Recipients) {
-          Recipient recipient = new Recipient();
-          recipient.Identifier = stringRecipient;
-          recipients.add(recipient);
+          notification.CallbackIds.add(notice.getUuid());
         }
 
-        notification.Recipients = recipients;
+        // populate the subject and body fields; if there is a problem
+        // generating the content, then mark the notices as FAILED
+        try {
+          renderDigestNotificationContent(notification, histories, target);
+
+          // dispatch
+          DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
+          m_executor.execute(runnable);
+        } catch (Exception exception) {
+          LOG.error("Unable to create notification for alerts", exception);
+
+          // there was a problem generating content for the target; mark all
+          // notices as FAILED and skip this target
+          // mark these as failed
+          notification.Callback.onFailure(notification.CallbackIds);
+        }
+      } else {
+        // the dispatcher does not support digest, each notice must have a 1:1
+        // notification created for it
+        for (AlertNoticeEntity notice : notices) {
+          Notification notification = buildNotificationFromTarget(target);
+          AlertHistoryEntity history = notice.getAlertHistory();
+          notification.CallbackIds = Collections.singletonList(notice.getUuid());
+
+          // populate the subject and body fields; if there is a problem
+          // generating the content, then mark the notices as FAILED
+          try {
+            renderNotificationContent(notification, history, target);
+
+            // dispatch
+            DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
+            m_executor.execute(runnable);
+          } catch (Exception exception) {
+            LOG.error("Unable to create notification for alert", exception);
+
+            // mark these as failed
+            notification.Callback.onFailure(notification.CallbackIds);
+          }
+        }
       }
-
-      // set all other dispatch properties
-      notification.DispatchProperties = properties;
-
-      // dispatch
-      NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);
-      DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
-
-      m_executor.execute(runnable);
     }
   }
 
@@ -372,37 +375,89 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
   }
 
   /**
-   * Generates the content for the {@link Notification} from the
-   * {@link #m_alertTemplates}. If there is a problem with the templates, this
-   * will fallback to non-formatted content.
+   * Initializes a {@link Notification} instance from an
+   * {@link AlertTargetEntity}. This method does most of the boilerplate work to
+   * get a {@link Notification} that is almost ready to send.
+   * <p/>
+   * The {@link Notification} will not have any of the callback IDs or content
+   * set.
+   *
+   * @param target
+   *          the alert target
+   * @return the initialized notification
+   */
+  private Notification buildNotificationFromTarget(AlertTargetEntity target) {
+    String propertiesJson = target.getProperties();
+
+    AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
+        AlertTargetProperties.class);
+
+    Map<String, String> properties = targetProperties.Properties;
+
+    // create an initialize the notification
+    Notification notification = new Notification();
+    notification.Callback = new AlertNoticeDispatchCallback();
+    notification.DispatchProperties = properties;
+
+    // set dispatch credentials
+    if (properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_USERNAME)
+        && properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_PASSWORD)) {
+      DispatchCredentials credentials = new DispatchCredentials();
+      credentials.UserName = properties.get(AMBARI_DISPATCH_CREDENTIAL_USERNAME);
+      credentials.Password = properties.get(AMBARI_DISPATCH_CREDENTIAL_PASSWORD);
+      notification.Credentials = credentials;
+    }
+
+    // create recipients
+    if (null != targetProperties.Recipients) {
+      List<Recipient> recipients = new ArrayList<Recipient>(
+          targetProperties.Recipients.size());
+
+      for (String stringRecipient : targetProperties.Recipients) {
+        Recipient recipient = new Recipient();
+        recipient.Identifier = stringRecipient;
+        recipients.add(recipient);
+      }
+
+      notification.Recipients = recipients;
+    }
+
+    return notification;
+  }
+
+  /**
+   * Generates digest content for the {@link Notification} using the
+   * {@link #m_alertTemplates} and the list of alerts passed in. If there is a
+   * problem with the templates, this will fallback to non-formatted content.
    *
    * @param notification
    *          the notification (not {@code null}).
    * @param histories
-   *          the alerts to generate the content fron (not {@code null}.
+   *          the alerts to generate the content from (not {@code null}.
    * @param target
    *          the target of the {@link Notification}.
    */
-  private void renderNotificationContent(Notification notification,
+  private void renderDigestNotificationContent(Notification notification,
       List<AlertHistoryEntity> histories, AlertTargetEntity target)
       throws IOException {
     String targetType = target.getNotificationType();
 
     // build the velocity objects for template rendering
     AmbariInfo ambari = new AmbariInfo(m_metaInfo.get());
-    AlertInfo summary = new AlertInfo(histories);
+    AlertSummaryInfo summary = new AlertSummaryInfo(histories);
     DispatchInfo dispatch = new DispatchInfo(target);
 
     // get the template for this target type
     final Writer subjectWriter = new StringWriter();
     final Writer bodyWriter = new StringWriter();
     final AlertTemplate template = m_alertTemplates.getTemplate(targetType);
+
     if (null != template) {
       // create the velocity context for template rendering
       VelocityContext velocityContext = new VelocityContext();
-      velocityContext.put("ambari", ambari);
-      velocityContext.put("summary", summary);
-      velocityContext.put("dispatch", dispatch);
+      velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
+      velocityContext.put(VELOCITY_SUMMARY_KEY, summary);
+      velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);
 
       // render the template and assign the content to the notification
       String subjectTemplate = template.getSubject();
@@ -433,6 +488,69 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
     notification.Body = bodyWriter.toString();
   }
 
+  /**
+   * Generates the content for the {@link Notification} using the
+   * {@link #m_alertTemplates} and the single alert passed in. If there is a
+   * problem with the templates, this will fallback to non-formatted content.
+   *
+   * @param notification
+   *          the notification (not {@code null}).
+   * @param history
+   *          the alert to generate the content from (not {@code null}.
+   * @param target
+   *          the target of the {@link Notification}.
+   */
+  private void renderNotificationContent(Notification notification,
+      AlertHistoryEntity history, AlertTargetEntity target) throws IOException {
+    String targetType = target.getNotificationType();
+
+    // build the velocity objects for template rendering
+    AmbariInfo ambari = new AmbariInfo(m_metaInfo.get());
+    AlertInfo alert = new AlertInfo(history);
+    DispatchInfo dispatch = new DispatchInfo(target);
+
+    // get the template for this target type
+    final Writer subjectWriter = new StringWriter();
+    final Writer bodyWriter = new StringWriter();
+    final AlertTemplate template = m_alertTemplates.getTemplate(targetType);
+
+    if (null != template) {
+      // create the velocity context for template rendering
+      VelocityContext velocityContext = new VelocityContext();
+      velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
+      velocityContext.put(VELOCITY_ALERT_KEY, alert);
+      velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);
+
+      // render the template and assign the content to the notification
+      String subjectTemplate = template.getSubject();
+      String bodyTemplate = template.getBody();
+
+      // render the subject
+      Velocity.evaluate(velocityContext, subjectWriter, VELOCITY_LOG_TAG,
+          subjectTemplate);
+
+      // render the body
+      Velocity.evaluate(velocityContext, bodyWriter, VELOCITY_LOG_TAG,
+          bodyTemplate);
+    } else {
+      // a null template is possible from parsing incorrectly or not
+      // having the correct type defined for the target
+      subjectWriter.write(alert.getAlertState().name());
+      subjectWriter.write(" ");
+      subjectWriter.write(alert.getAlertName());
+
+      bodyWriter.write(alert.getAlertState().name());
+      bodyWriter.write(" ");
+      bodyWriter.write(alert.getAlertName());
+      bodyWriter.write(" ");
+      bodyWriter.write(alert.getAlertText());
+      bodyWriter.write("\n");
+    }
+
+    notification.Subject = subjectWriter.toString();
+    notification.Body = bodyWriter.toString();
+  }
+
   /**
    * The {@link AlertTargetProperties} separates out the dispatcher properties
    * from the list of recipients which is a JSON array and not a String.
@@ -476,7 +594,8 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
         JsonElement entryValue = entry.getValue();
 
         if (entryKey.equals(AMBARI_DISPATCH_RECIPIENTS)) {
-          Type listType = new TypeToken<List<String>>() {}.getType();
+          Type listType = new TypeToken<List<String>>() {
+          }.getType();
           JsonArray jsonArray = entryValue.getAsJsonArray();
           properties.Recipients = context.deserialize(jsonArray, listType);
         } else {
@@ -519,8 +638,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
    * the dispatch framework and then update the {@link AlertNoticeEntity}
    * {@link NotificationState}.
    */
-  private final class AlertNoticeDispatchCallback implements
-      DispatchCallback {
+  private final class AlertNoticeDispatchCallback implements DispatchCallback {
 
     /**
      * {@inheritDoc}
@@ -568,11 +686,103 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
   }
 
   /**
-   * The {@link AlertInfo} class encapsulates all of the alert information for
-   * the {@link Notification}. This includes customized structures to better
-   * organize information about each of the services, hosts, and alert states.
+   * The {@link AlertInfo} class encapsulates all information about a single
+   * alert for a single outbound {@link Notification}.
    */
   public final static class AlertInfo {
+    private final AlertHistoryEntity m_history;
+
+    /**
+     * Constructor.
+     *
+     * @param history
+     */
+    protected AlertInfo(AlertHistoryEntity history) {
+      m_history = history;
+    }
+
+    /**
+     * Gets the host name or {@code null} if none.
+     *
+     * @return
+     */
+    public String getHostName() {
+      return m_history.getHostName();
+    }
+
+    /**
+     * Gets whether there is a host associated with the alert. Some alerts like
+     * aggregate alerts don't have hosts.
+     *
+     * @return
+     */
+    public boolean hasHostName() {
+      return m_history.getHostName() != null;
+    }
+
+    /**
+     * Gets the service name or {@code null} if none.
+     *
+     * @return
+     */
+    public String getServiceName() {
+      return m_history.getServiceName();
+    }
+
+    /**
+     * Gets the service component name, or {@code null} if none.
+     *
+     * @return
+     */
+    public String getComponentName() {
+      return m_history.getComponentName();
+    }
+
+    /**
+     * Gets whether there is a component associated with an alert. Some alerts
+     * don't have an associated component.
+     *
+     * @return
+     */
+    public boolean hasComponentName() {
+      return m_history.getComponentName() != null;
+    }
+
+    /**
+     * Gets the state of the alert.
+     *
+     * @return
+     */
+    public AlertState getAlertState() {
+      return m_history.getAlertState();
+    }
+
+    /**
+     * Gets the descriptive name of the alert.
+     *
+     * @return
+     */
+    public String getAlertName() {
+      return m_history.getAlertDefinition().getLabel();
+    }
+
+    /**
+     * Gets the text of the alert.
+     *
+     * @return
+     */
+    public String getAlertText() {
+      return m_history.getAlertText();
+    }
+  }
+
+  /**
+   * The {@link AlertSummaryInfo} class encapsulates all of the alert
+   * information for the {@link Notification}. This includes customized
+   * structures to better organize information about each of the services,
+   * hosts, and alert states.
+   */
+  public final static class AlertSummaryInfo {
     private int m_okCount = 0;
     private int m_warningCount = 0;
     private int m_criticalCount = 0;
@@ -597,8 +807,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
      * A mapping of service to alerts where the alerts are also grouped by state
      * for that service.
      */
-    private final Map<String, Map<AlertState, List<AlertHistoryEntity>>> m_alertsByServiceAndState =
-        new HashMap<String, Map<AlertState, List<AlertHistoryEntity>>>();
+    private final Map<String, Map<AlertState, List<AlertHistoryEntity>>> m_alertsByServiceAndState = new HashMap<String, Map<AlertState, List<AlertHistoryEntity>>>();
 
     /**
      * A mapping of all services by state.
@@ -615,7 +824,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
      *
      * @param histories
      */
-    protected AlertInfo(List<AlertHistoryEntity> histories) {
+    protected AlertSummaryInfo(List<AlertHistoryEntity> histories) {
       m_alerts = histories;
 
       // group all alerts by their service and severity
@@ -624,7 +833,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
         String serviceName = history.getServiceName();
         String hostName = history.getHostName();
 
-        if( null != hostName ){
+        if (null != hostName) {
           m_hosts.add(hostName);
         }
 

+ 9 - 7
ambari-server/src/main/resources/alert-templates.xml

@@ -177,18 +177,20 @@
   </alert-template>
   <alert-template type="SNMP">
     <subject>
-      <![CDATA[Alert Summary: OK[$summary.getOkCount()], Warning[$summary.getWarningCount()], Critical[$summary.getCriticalCount()], Unknown[$summary.getUnknownCount()]]]>
+      <![CDATA[[$alert.getAlertState()] $alert.getAlertName()]]>
     </subject>
     <body>
       <![CDATA[
-#set( $alertStates = ["OK", "WARNING", "CRITICAL", "UNKNOWN"] )
-#set( $services = $summary.getServices() )
-#foreach( $service in $services )
-#foreach( $alert in $summary.getAlerts($service) )
-[$service] [$alert.getAlertState()] [$alert.getAlertDefinition().getLabel()] [$alert.getAlertText()]
+[Alert] $alert.getAlertName()
+[Service] $alert.getServiceName()
+#if( $alert.hasComponentName() )
+[Component] $alert.getComponentName()
 #end
+#if( $alert.hasHostName() )
+[Host] $alert.getHostName()
 #end
-      ]]>
+
+$alert.getAlertText()]]>
     </body>
   </alert-template>  
 </alert-templates>

+ 2 - 2
ambari-server/src/main/resources/common-services/FALCON/0.5.0.2.1/alerts.json

@@ -42,10 +42,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 4 - 4
ambari-server/src/main/resources/common-services/HDFS/2.1.0.2.0/alerts.json

@@ -95,10 +95,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -447,10 +447,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 2 - 2
ambari-server/src/main/resources/common-services/OOZIE/4.0.0.2.0/alerts.json

@@ -15,10 +15,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 2 - 2
ambari-server/src/main/resources/common-services/STORM/0.9.1.2.1/alerts.json

@@ -65,10 +65,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 4 - 4
ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json

@@ -95,10 +95,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -447,10 +447,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 2 - 2
ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/OOZIE/alerts.json

@@ -15,10 +15,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 8 - 8
ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/YARN/alerts.json

@@ -18,10 +18,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -179,10 +179,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -220,10 +220,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -324,10 +324,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 4 - 4
ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json

@@ -95,10 +95,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -422,10 +422,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 4 - 4
ambari-server/src/main/resources/stacks/HDP/1.3.2/services/MAPREDUCE/alerts.json

@@ -42,10 +42,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -205,10 +205,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 2 - 2
ambari-server/src/main/resources/stacks/HDP/1.3.2/services/OOZIE/alerts.json

@@ -15,10 +15,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 8 - 8
ambari-server/src/main/resources/stacks/HDP/2.0.6/services/YARN/alerts.json

@@ -18,10 +18,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -179,10 +179,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -220,10 +220,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"
@@ -325,10 +325,10 @@
           },
           "reporting": {
             "ok": {
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "warning":{
-              "text": "HTTP {0} response in {2:.4f} seconds"
+              "text": "HTTP {0} response in {2:.3f} seconds"
             },
             "critical": {
               "text": "Connection failed to {1}"

+ 1 - 1
ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java

@@ -77,7 +77,7 @@ public class EmailDispatcherTest {
    * Tests that an email without properties causes a callback error.
    */
   @Test
-  public void testNoEmailPropeties() {
+  public void testNoEmailProperties() {
     Notification notification = new Notification();
     DispatchCallback callback = EasyMock.createMock(DispatchCallback.class);
     notification.Callback = callback;

+ 8 - 0
ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java

@@ -37,6 +37,14 @@ public class MockDispatcher implements NotificationDispatcher {
     return "MOCK";
   }
 
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean isDigestSupported() {
+    return true;
+  }
+
   /**
    * {@inheritDoc}
    */

+ 176 - 19
ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java

@@ -62,7 +62,8 @@ import com.google.inject.util.Modules;
  */
 public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
 
-  final static String ALERT_NOTICE_UUID = UUID.randomUUID().toString();
+  final static String ALERT_NOTICE_UUID_1 = UUID.randomUUID().toString();
+  final static String ALERT_NOTICE_UUID_2 = UUID.randomUUID().toString();
   final static String ALERT_UNIQUE_TEXT = "0eeda438-2b13-4869-a416-137e35ff76e9";
   final static String HOSTNAME = "c6401.ambari.apache.org";
   final static Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
@@ -133,13 +134,34 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
 
   /**
    * Tests the parsing of the {@link AlertHistoryEntity} list into
-   * {@link AlertInfo}.
+   * {@link AlertSummaryInfo}.
    *
    * @throws Exception
    */
   @Test
   public void testAlertInfo() throws Exception {
-    AlertInfo alertInfo = new AlertInfo(m_histories);
+    AlertHistoryEntity history = m_histories.get(0);
+    AlertInfo alertInfo = new AlertInfo(history);
+    assertEquals(history.getAlertDefinition().getLabel(), alertInfo.getAlertName());
+    assertEquals(history.getAlertState(), alertInfo.getAlertState());
+    assertEquals(history.getAlertText(), alertInfo.getAlertText());
+    assertEquals(history.getComponentName(), alertInfo.getComponentName());
+    assertEquals(history.getHostName(), alertInfo.getHostName());
+    assertEquals(history.getServiceName(), alertInfo.getServiceName());
+
+    assertEquals(false, alertInfo.hasComponentName());
+    assertEquals(true, alertInfo.hasHostName());
+  }
+
+  /**
+   * Tests the parsing of the {@link AlertHistoryEntity} list into
+   * {@link AlertSummaryInfo}.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testAlertSummaryInfo() throws Exception {
+    AlertSummaryInfo alertInfo = new AlertSummaryInfo(m_histories);
     assertEquals(50, alertInfo.getAlerts().size());
     assertEquals(10, alertInfo.getAlerts("Service 1").size());
     assertEquals(10, alertInfo.getAlerts("Service 2").size());
@@ -176,18 +198,16 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
   }
 
   /**
-   * Tests that the dispatcher is not called when there are no notices.
+   * Tests a digest dispatch for email.
    *
    * @throws Exception
    */
   @Test
-  public void testDispatch() throws Exception {
-    MockDispatcher dispatcher = new MockDispatcher();
+  public void testDigestDispatch() throws Exception {
+    MockEmailDispatcher dispatcher = new MockEmailDispatcher();
 
-    EasyMock.expect(m_dao.findPendingNotices()).andReturn(getMockNotices()).once();
-
-    EasyMock.expect(m_dispatchFactory.getDispatcher("EMAIL")).andReturn(
-        dispatcher).once();
+    EasyMock.expect(m_dao.findPendingNotices()).andReturn(getSingleEmailMockNotice()).once();
+    EasyMock.expect(m_dispatchFactory.getDispatcher("EMAIL")).andReturn(dispatcher).once();
 
     EasyMock.replay(m_dao, m_dispatchFactory);
 
@@ -209,6 +229,35 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
     assertTrue(notification.Body.contains(ALERT_UNIQUE_TEXT));
   }
 
+  /**
+   * Tests a digest dispatch for SNMP.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testSingleDispatch() throws Exception {
+    MockSnmpDispatcher dispatcher = new MockSnmpDispatcher();
+
+    EasyMock.expect(m_dao.findPendingNotices()).andReturn(getSnmpMockNotices()).once();
+    EasyMock.expect(m_dispatchFactory.getDispatcher("SNMP")).andReturn(
+        dispatcher).atLeastOnce();
+
+    EasyMock.replay(m_dao, m_dispatchFactory);
+
+    // "startup" the service so that its initialization is done
+    AlertNoticeDispatchService service = m_injector.getInstance(AlertNoticeDispatchService.class);
+    service.startUp();
+
+    // service trigger with mock executor that blocks
+    service.setExecutor(new MockExecutor());
+    service.runOneIteration();
+
+    EasyMock.verify(m_dao, m_dispatchFactory);
+
+    List<Notification> notifications = dispatcher.getNotifications();
+    assertEquals(2, notifications.size());
+  }
+
   /**
    * Tests that a failed dispatch invokes the callback to mark the UUIDs of the
    * notices as FAILED.
@@ -217,14 +266,17 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
    */
   @Test
   public void testFailedDispatch() throws Exception {
-    MockDispatcher dispatcher = new MockDispatcher();
-    List<AlertNoticeEntity> notices = getMockNotices();
+    MockEmailDispatcher dispatcher = new MockEmailDispatcher();
+    List<AlertNoticeEntity> notices = getSingleEmailMockNotice();
     AlertNoticeEntity notice = notices.get(0);
 
     // these expectations happen b/c we need to mark the notice as FAILED
     EasyMock.expect(m_dao.findPendingNotices()).andReturn(notices).once();
-    EasyMock.expect(m_dao.findNoticeByUuid(ALERT_NOTICE_UUID)).andReturn(notice).once();
-    EasyMock.expect(m_dao.merge(getMockNotices().get(0))).andReturn(notice).once();
+    EasyMock.expect(m_dao.findNoticeByUuid(ALERT_NOTICE_UUID_1)).andReturn(notice).once();
+    EasyMock.expect(m_dao.merge(getSingleEmailMockNotice().get(0))).andReturn(notice).once();
+
+    EasyMock.expect(m_dispatchFactory.getDispatcher("EMAIL")).andReturn(
+        dispatcher).once();
 
     EasyMock.replay(m_dao, m_dispatchFactory);
 
@@ -242,11 +294,11 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
   }
 
   /**
-   * Gets PENDING notices.
+   * Gets a single PENDING notice.
    *
    * @return
    */
-  private List<AlertNoticeEntity> getMockNotices(){
+  private List<AlertNoticeEntity> getSingleEmailMockNotice() {
     AlertHistoryEntity history = new AlertHistoryEntity();
     history.setServiceName("HDFS");
     history.setClusterId(1L);
@@ -257,6 +309,7 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
     history.setAlertTimestamp(System.currentTimeMillis());
 
     AlertTargetEntity target = new AlertTargetEntity();
+    target.setTargetId(1L);
     target.setAlertStates(EnumSet.allOf(AlertState.class));
     target.setTargetName("Alert Target");
     target.setDescription("Mock Target");
@@ -266,7 +319,7 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
     target.setProperties(properties);
 
     AlertNoticeEntity notice = new AlertNoticeEntity();
-    notice.setUuid(ALERT_NOTICE_UUID);
+    notice.setUuid(ALERT_NOTICE_UUID_1);
     notice.setAlertTarget(target);
     notice.setAlertHistory(history);
     notice.setNotifyState(NotificationState.PENDING);
@@ -277,10 +330,68 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
     return notices;
   }
 
+  /**
+   * Gets 2 PENDING notices for SNMP.
+   *
+   * @return
+   */
+  private List<AlertNoticeEntity> getSnmpMockNotices() {
+    AlertDefinitionEntity definition = new AlertDefinitionEntity();
+    definition.setDefinitionId(1L);
+    definition.setDefinitionName("alert-definition-1");
+    definition.setLabel("Alert Definition 1");
+
+    AlertHistoryEntity history1 = new AlertHistoryEntity();
+    history1.setAlertDefinition(definition);
+    history1.setServiceName("HDFS");
+    history1.setClusterId(1L);
+    history1.setAlertLabel("Label");
+    history1.setAlertState(AlertState.OK);
+    history1.setAlertText(ALERT_UNIQUE_TEXT);
+    history1.setAlertTimestamp(System.currentTimeMillis());
+
+    AlertHistoryEntity history2 = new AlertHistoryEntity();
+    history2.setAlertDefinition(definition);
+    history2.setServiceName("HDFS");
+    history2.setClusterId(1L);
+    history2.setAlertLabel("Label");
+    history2.setAlertState(AlertState.CRITICAL);
+    history2.setAlertText(ALERT_UNIQUE_TEXT + " CRITICAL");
+    history2.setAlertTimestamp(System.currentTimeMillis());
+
+    AlertTargetEntity target = new AlertTargetEntity();
+    target.setTargetId(1L);
+    target.setAlertStates(EnumSet.allOf(AlertState.class));
+    target.setTargetName("Alert Target");
+    target.setDescription("Mock Target");
+    target.setNotificationType("SNMP");
+
+    String properties = "{ \"foo\" : \"bar\" }";
+    target.setProperties(properties);
+
+    AlertNoticeEntity notice1 = new AlertNoticeEntity();
+    notice1.setUuid(ALERT_NOTICE_UUID_1);
+    notice1.setAlertTarget(target);
+    notice1.setAlertHistory(history1);
+    notice1.setNotifyState(NotificationState.PENDING);
+
+    AlertNoticeEntity notice2 = new AlertNoticeEntity();
+    notice2.setUuid(ALERT_NOTICE_UUID_2);
+    notice2.setAlertTarget(target);
+    notice2.setAlertHistory(history2);
+    notice2.setNotifyState(NotificationState.PENDING);
+
+    ArrayList<AlertNoticeEntity> notices = new ArrayList<AlertNoticeEntity>();
+    notices.add(notice1);
+    notices.add(notice2);
+
+    return notices;
+  }
+
   /**
    * A mock dispatcher that captures the {@link Notification}.
    */
-  private static final class MockDispatcher implements NotificationDispatcher {
+  private static final class MockEmailDispatcher implements NotificationDispatcher {
 
     private Notification m_notificaiton;
 
@@ -289,7 +400,15 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
      */
     @Override
     public String getType() {
-      return null;
+      return "EMAIL";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isDigestSupported() {
+      return true;
     }
 
     /**
@@ -305,6 +424,43 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
     }
   }
 
+  /**
+   * A mock dispatcher that captures the {@link Notification}.
+   */
+  private static final class MockSnmpDispatcher implements
+      NotificationDispatcher {
+
+    private List<Notification> m_notifications = new ArrayList<Notification>();
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getType() {
+      return "SNMP";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isDigestSupported() {
+      return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dispatch(Notification notification) {
+      m_notifications.add(notification);
+    }
+
+    public List<Notification> getNotifications() {
+      return m_notifications;
+    }
+  }
+
   /**
    * An {@link Executor} that calls {@link Runnable#run()} directly in the
    * current thread.
@@ -334,6 +490,7 @@ public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
       binder.bind(AmbariMetaInfo.class).toInstance(m_metaInfo);
 
       EasyMock.expect(m_metaInfo.getServerVersion()).andReturn("2.0.0").anyTimes();
+      EasyMock.replay(m_metaInfo);
     }
   }
 }