You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by jo...@apache.org on 2014/11/21 14:50:48 UTC
ambari git commit: AMBARI-8400 - Alerts: Template Engine for
Dispatching (jonathanhurley)
Repository: ambari
Updated Branches:
refs/heads/trunk 15ffd2ece -> 650e9d437
AMBARI-8400 - Alerts: Template Engine for Dispatching (jonathanhurley)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/650e9d43
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/650e9d43
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/650e9d43
Branch: refs/heads/trunk
Commit: 650e9d4372d0f9b6a1cec8b64ae8f09b4d565728
Parents: 15ffd2e
Author: Jonathan Hurley <jh...@hortonworks.com>
Authored: Thu Nov 20 13:53:34 2014 -0500
Committer: Jonathan Hurley <jh...@hortonworks.com>
Committed: Fri Nov 21 08:50:39 2014 -0500
----------------------------------------------------------------------
.../org/apache/ambari/server/AmbariService.java | 43 ++
.../render/AlertSummaryGroupedRenderer.java | 2 +-
.../server/configuration/Configuration.java | 19 +-
.../server/controller/ControllerModule.java | 71 +-
.../server/notifications/DispatchFactory.java | 2 +-
.../dispatchers/EmailDispatcher.java | 6 +-
.../services/AlertNoticeDispatchService.java | 659 +++++++++++++++++--
.../src/main/resources/alert-templates.xml | 144 ++++
.../stacks/BIGTOP/0.8/services/HDFS/alerts.json | 2 +-
.../stacks/HDP/1.3.2/services/HDFS/alerts.json | 2 +-
.../stacks/HDP/2.0.6/services/HDFS/alerts.json | 4 +-
.../AlertNoticeDispatchServiceTest.java | 339 ++++++++++
12 files changed, 1206 insertions(+), 87 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/AmbariService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/AmbariService.java b/ambari-server/src/main/java/org/apache/ambari/server/AmbariService.java
new file mode 100644
index 0000000..186e272
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/AmbariService.java
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.google.common.util.concurrent.AbstractScheduledService;
+import com.google.common.util.concurrent.ServiceManager;
+import com.google.inject.ScopeAnnotation;
+import com.google.inject.Singleton;
+
+/**
+ * The {@link AmbariService} annotation is used to register a class that
+ * implements Guava's {@link AbstractScheduledService} with the
+ * {@link ServiceManager}.
+ * <p/>
+ * Classes with this annotation are bound as singletons and automatically
+ * injected with their members. There is not need to use {@link Singleton} or
+ * {@link EagerSingleton}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE })
+@ScopeAnnotation
+public @interface AmbariService {
+}
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/api/query/render/AlertSummaryGroupedRenderer.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/query/render/AlertSummaryGroupedRenderer.java b/ambari-server/src/main/java/org/apache/ambari/server/api/query/render/AlertSummaryGroupedRenderer.java
index 0dbeb5c..52608b2 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/query/render/AlertSummaryGroupedRenderer.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/api/query/render/AlertSummaryGroupedRenderer.java
@@ -120,7 +120,7 @@ public class AlertSummaryGroupedRenderer extends AlertSummaryRenderer {
for (TreeNode<Resource> node : resultTree.getChildren()) {
Resource resource = node.getObject();
- Long definitionId = (Long) resource.getPropertyValue(AlertResourceProvider.ALERT_ID);
+ Long definitionId = (Long) resource.getPropertyValue(AlertResourceProvider.ALERT_DEFINITION_ID);
String definitionName = (String) resource.getPropertyValue(AlertResourceProvider.ALERT_DEFINITION_NAME);
AlertState state = (AlertState) resource.getPropertyValue(AlertResourceProvider.ALERT_STATE);
Long originalTimestampObject = (Long) resource.getPropertyValue(AlertResourceProvider.ALERT_ORIGINAL_TIMESTAMP);
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
index 0a96193..73a353e 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
@@ -331,8 +331,14 @@ public class Configuration {
private static final String SERVER_HTTP_SESSION_INACTIVE_TIMEOUT = "server.http.session.inactive_timeout";
+ /**
+ * The full path to the XML file that describes the different alert templates.
+ */
+ private static final String ALERT_TEMPLATE_FILE = "alerts.template.file";
+
private static final Logger LOG = LoggerFactory.getLogger(
Configuration.class);
+
private Properties properties;
private Map<String, String> configsMap;
private CredentialProvider credentialProvider = null;
@@ -810,8 +816,9 @@ public class Configuration {
String passwdProp = properties.getProperty(SCOM_JDBC_SINK_USER_PASSWD_KEY);
if (passwdProp != null) {
String dbpasswd = readPasswordFromStore(passwdProp);
- if (dbpasswd != null)
+ if (dbpasswd != null) {
return dbpasswd;
+ }
}
return readPasswordFromFile(passwdProp, SCOM_JDBC_SINK_USER_PASSWD_DEFAULT);
}
@@ -1209,4 +1216,14 @@ public class Configuration {
SERVER_HTTP_SESSION_INACTIVE_TIMEOUT,
"1800"));
}
+
+ /**
+ * Gets the location of the XML alert template file which contains the
+ * velocity templates for outbound notifications.
+ *
+ * @return the location of the template file, or {@code null} if not defined.
+ */
+ public String getAlertTemplateFile() {
+ return properties.getProperty(ALERT_TEMPLATE_FILE);
+ }
}
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
index 2d91462..a02f49d 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
@@ -34,6 +34,7 @@ import static org.eclipse.persistence.config.PersistenceUnitProperties.THROW_EXC
import java.lang.annotation.Annotation;
import java.security.SecureRandom;
+import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@@ -42,6 +43,7 @@ import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
+import org.apache.ambari.server.AmbariService;
import org.apache.ambari.server.EagerSingleton;
import org.apache.ambari.server.StaticallyInject;
import org.apache.ambari.server.actionmanager.ActionDBAccessor;
@@ -91,7 +93,6 @@ import org.apache.ambari.server.state.host.HostImpl;
import org.apache.ambari.server.state.scheduler.RequestExecution;
import org.apache.ambari.server.state.scheduler.RequestExecutionFactory;
import org.apache.ambari.server.state.scheduler.RequestExecutionImpl;
-import org.apache.ambari.server.state.services.AlertNoticeDispatchService;
import org.apache.ambari.server.state.stack.OsFamily;
import org.apache.ambari.server.state.svccomphost.ServiceComponentHostImpl;
import org.apache.ambari.server.view.ViewInstanceHandlerList;
@@ -109,6 +110,7 @@ import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.util.ClassUtils;
import org.springframework.web.filter.DelegatingFilterProxy;
+import com.google.common.util.concurrent.AbstractScheduledService;
import com.google.common.util.concurrent.ServiceManager;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -242,7 +244,6 @@ public class ControllerModule extends AbstractModule {
requestStaticInjection(ExecutionCommandWrapper.class);
- bindServices();
bindByAnnotation();
}
@@ -324,46 +325,26 @@ public class ControllerModule extends AbstractModule {
}
/**
- * Bind all {@link com.google.common.util.concurrent.Service} singleton
- * instances and then register them with a singleton {@link ServiceManager}.
- */
- private void bindServices() {
- Set<com.google.common.util.concurrent.Service> services =
- new HashSet<com.google.common.util.concurrent.Service>();
-
- AlertNoticeDispatchService alertNoticeDispatchService =
- new AlertNoticeDispatchService();
-
- bind(AlertNoticeDispatchService.class).toInstance(
- alertNoticeDispatchService);
-
- services.add(alertNoticeDispatchService);
- ServiceManager manager = new ServiceManager(services);
- bind(ServiceManager.class).toInstance(manager);
- }
-
- /**
- * Initializes specially-marked interfaces that require injection. All eager singletons that should be instantiated as soon as
- * possible and not wait for injection.
+ * Initializes specially-marked interfaces that require injection.
* <p/>
* An example of where this is needed is with a singleton that is headless; in
* other words, it doesn't have any injections but still needs to be part of
* the Guice framework.
* <p/>
- * <p/>
* A second example of where this is needed is when classes require static
* members that are available via injection.
* <p/>
* This currently scans {@code org.apache.ambari.server} for any
- * {@link EagerSingleton} or {@link StaticallyInject} instances.
+ * {@link EagerSingleton} or {@link StaticallyInject} or {@link AmbariService}
+ * instances.
*/
+ @SuppressWarnings("unchecked")
private void bindByAnnotation() {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
- @SuppressWarnings("unchecked")
- List<Class<? extends Annotation>> classes = Arrays.asList(EagerSingleton.class,
- StaticallyInject.class);
+ List<Class<? extends Annotation>> classes = Arrays.asList(
+ EagerSingleton.class, StaticallyInject.class, AmbariService.class);
// match only singletons that are eager listeners
for (Class<? extends Annotation> cls : classes) {
@@ -377,6 +358,9 @@ public class ControllerModule extends AbstractModule {
return;
}
+ Set<com.google.common.util.concurrent.Service> services =
+ new HashSet<com.google.common.util.concurrent.Service>();
+
for (BeanDefinition beanDefinition : beanDefinitions) {
String className = beanDefinition.getBeanClassName();
Class<?> clazz = ClassUtils.resolveClassName(className,
@@ -391,7 +375,34 @@ public class ControllerModule extends AbstractModule {
requestStaticInjection(clazz);
LOG.debug("Statically injecting {} ", clazz);
}
+
+ // Ambari services are registered with Guava
+ if (null != clazz.getAnnotation(AmbariService.class)) {
+ // safety check to ensure it's actually a Guava service
+ if (!AbstractScheduledService.class.isAssignableFrom(clazz)) {
+ String message = MessageFormat.format(
+ "Unable to register service {0} because it is not an AbstractScheduledService",
+ clazz);
+
+ LOG.warn(message);
+ throw new RuntimeException(message);
+ }
+
+ // instantiate the service, register as singleton via toInstance()
+ AbstractScheduledService service = null;
+ try {
+ service = (AbstractScheduledService) clazz.newInstance();
+ bind((Class<AbstractScheduledService>) clazz).toInstance(service);
+ services.add(service);
+ LOG.debug("Registering service {} ", clazz);
+ } catch (Exception exception) {
+ LOG.error("Unable to register {} as a service", clazz, exception);
+ throw new RuntimeException(exception);
+ }
+ }
}
- }
-}
+ ServiceManager manager = new ServiceManager(services);
+ bind(ServiceManager.class).toInstance(manager);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
index 13f2da2..e690e76 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
@@ -31,7 +31,7 @@ import com.google.inject.Singleton;
* {@link NotificationDispatcher} based on a supplied type.
*/
@Singleton
-public final class DispatchFactory {
+public class DispatchFactory {
/**
* Mapping of dispatch type to dispatcher singleton.
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
index a5dad84..f979c03 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
@@ -22,7 +22,6 @@ import java.util.Properties;
import java.util.Timer;
import javax.mail.Authenticator;
-import javax.mail.Message;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
@@ -113,7 +112,8 @@ public class EmailDispatcher implements NotificationDispatcher {
session = Session.getInstance(properties, authenticator);
try {
- Message message = new MimeMessage(session);
+ // !!! at some point in the future we can worry about multipart
+ MimeMessage message = new MimeMessage(session);
for (Recipient recipient : notification.Recipients) {
InternetAddress address = new InternetAddress(recipient.Identifier);
@@ -121,7 +121,7 @@ public class EmailDispatcher implements NotificationDispatcher {
}
message.setSubject(notification.Subject);
- message.setText(notification.Body);
+ message.setText(notification.Body, "UTF-8", "html");
Transport.send(message);
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java b/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
index 72487b3..69f3393 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
@@ -17,20 +17,36 @@
*/
package org.apache.ambari.server.state.services;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.io.Writer;
import java.lang.reflect.Type;
-import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.apache.ambari.server.AmbariService;
+import org.apache.ambari.server.api.services.AmbariMetaInfo;
+import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.events.AlertEvent;
import org.apache.ambari.server.notifications.DispatchCallback;
import org.apache.ambari.server.notifications.DispatchCredentials;
@@ -45,6 +61,9 @@ import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
import org.apache.ambari.server.orm.entities.AlertTargetEntity;
import org.apache.ambari.server.state.AlertState;
import org.apache.ambari.server.state.NotificationState;
+import org.apache.commons.io.IOUtils;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,7 +78,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import com.google.inject.Provider;
/**
* The {@link AlertNoticeDispatchService} is used to scan the database for
@@ -69,16 +88,32 @@ import com.google.inject.Singleton;
* The dispatch system will then make a callback to
* {@link AlertNoticeDispatchCallback} so that the {@link NotificationState} can
* be updated to its final value.
+ * <p/>
+ * This class uses the templates that are defined via
+ * {@link Configuration#getAlertTemplateFile()} or the fallback internal
+ * template {@code alert-templates.xml}. These files are parsed during
+ * {@link #startUp()}. If there is a problem parsing them, the service will
+ * still startup normally, producing an error in logs. It will fall back to
+ * simple string concatenation for {@link Notification} content in this case.
*/
-@Singleton
+@AmbariService
public class AlertNoticeDispatchService extends AbstractScheduledService {
-
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(AlertNoticeDispatchService.class);
/**
+ * The log tag to pass to Apache Velocity during rendering.
+ */
+ private static final String VELOCITY_LOG_TAG = "ambari-alerts";
+
+ /**
+ * The internal Ambari templates that ship.
+ */
+ private static final String AMBARI_ALERT_TEMPLATES = "alert-templates.xml";
+
+ /**
* The property containing the dispatch authentication username.
*/
private static final String AMBARI_DISPATCH_CREDENTIAL_USERNAME = "ambari.dispatch.credential.username";
@@ -105,16 +140,34 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
private AlertDispatchDAO m_dao;
/**
- * The factory used to get an {@link NotificationDispatcher} instance to submit to the
- * {@link #m_executor}.
+ * The factory used to get an {@link NotificationDispatcher} instance to
+ * submit to the {@link #m_executor}.
*/
@Inject
private DispatchFactory m_dispatchFactory;
/**
+ * The alert templates to use when rendering content for a
+ * {@link Notification}.
+ */
+ private AlertTemplates m_alertTemplates;
+
+ /**
+ * The configuration instance to get Ambari properties.
+ */
+ @Inject
+ private Configuration m_configuration;
+
+ /**
+ * Ambari meta information used fro alert {@link Notification}s.
+ */
+ @Inject
+ private Provider<AmbariMetaInfo> m_metaInfo;
+
+ /**
* The executor responsible for dispatching.
*/
- private final ThreadPoolExecutor m_executor;
+ private Executor m_executor;
/**
* Constructor.
@@ -134,6 +187,67 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
/**
* {@inheritDoc}
+ * <p/>
+ * Parse the XML template for {@link Notification} content. If there is a
+ * problem parsing the content, the service will still startup normally but
+ * the {@link Notification} content will fallback to plaintext.
+ */
+ @Override
+ protected void startUp() throws Exception {
+ super.startUp();
+
+ InputStream inputStream = null;
+ String alertTemplatesFile = null;
+
+ try {
+ alertTemplatesFile = m_configuration.getAlertTemplateFile();
+ if (null != alertTemplatesFile) {
+ File file = new File(alertTemplatesFile);
+ inputStream = new FileInputStream(file);
+ }
+ } catch (Exception exception) {
+ LOG.warn("Unable to load alert template file {}", alertTemplatesFile,
+ exception);
+ }
+
+ try {
+ JAXBContext context = JAXBContext.newInstance(AlertTemplates.class);
+ Unmarshaller unmarshaller = context.createUnmarshaller();
+
+ // if the file provided via the configuration is not available, use
+ // the internal one
+ if (null == inputStream) {
+ inputStream = ClassLoader.getSystemResourceAsStream(AMBARI_ALERT_TEMPLATES);
+ }
+
+ m_alertTemplates = (AlertTemplates) unmarshaller.unmarshal(inputStream);
+ } catch (Exception exception) {
+ LOG.error(
+ "Unable to load alert template file {}, outbound notifications will not be formatted",
+ AMBARI_ALERT_TEMPLATES,
+ exception);
+ } finally {
+ if (null != inputStream) {
+ IOUtils.closeQuietly(inputStream);
+ }
+ }
+ }
+
+
+ /**
+ * Sets the {@link Executor} to use when dispatching {@link Notification}s.
+ * This should only be used by unit tests to provide a mock executor.
+ *
+ * @param executor
+ * the executor to use (not {@code null).
+
+ */
+ protected void setExecutor(Executor executor) {
+ m_executor = executor;
+ }
+
+ /**
+ * {@inheritDoc}
*/
@Override
protected void runOneIteration() throws Exception {
@@ -145,10 +259,10 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
LOG.info("There are {} pending alert notices about to be dispatched..."
+ pending.size());
- // combine all histories by target
- Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap = new HashMap<AlertTargetEntity, List<AlertNoticeEntity>>(
- pending.size());
+ Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap =
+ new HashMap<AlertTargetEntity, List<AlertNoticeEntity>>(pending.size());
+ // combine all histories by target
for (AlertNoticeEntity notice : pending) {
AlertTargetEntity target = notice.getAlertTarget();
@@ -161,6 +275,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
notices.add(notice);
}
+ // now that all of the notices are grouped by target, dispatch them
Set<AlertTargetEntity> targets = aggregateMap.keySet();
for (AlertTargetEntity target : targets) {
List<AlertNoticeEntity> notices = aggregateMap.get(target);
@@ -168,7 +283,9 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
continue;
}
+ String targetType = target.getNotificationType();
String propertiesJson = target.getProperties();
+
AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
AlertTargetProperties.class);
@@ -178,47 +295,37 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
notification.Callback = new AlertNoticeDispatchCallback();
notification.CallbackIds = new ArrayList<String>(notices.size());
- // !!! FIXME: temporary until velocity templates are implemented
- String subject = "OK ({0}), Warning ({1}), Critical ({2})";
- StringBuilder buffer = new StringBuilder(512);
-
- int okCount = 0;
- int warningCount = 0;
- int criticalCount = 0;
+ 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());
+ }
- AlertState alertState = history.getAlertState();
- switch (alertState) {
- case CRITICAL:
- criticalCount++;
- break;
- case OK:
- okCount++;
- break;
- case UNKNOWN:
- // !!! hmmmmmm
- break;
- case WARNING:
- warningCount++;
- break;
- default:
- break;
+ // 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);
+
+ // 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());
+ for (AlertNoticeEntity notice : notices) {
+ failedNoticeIds.add(notice.getUuid());
}
- buffer.append(history.getAlertLabel());
- buffer.append(": ");
- buffer.append(history.getAlertText());
- buffer.append("\n");
+ // mark these as failed
+ notification.Callback.onFailure(failedNoticeIds);
+ continue;
}
- notification.Subject = MessageFormat.format(subject, okCount,
- warningCount, criticalCount);
-
- notification.Body = buffer.toString();
-
// set dispatch credentials
if (properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_USERNAME)
&& properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_PASSWORD)) {
@@ -228,6 +335,7 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
notification.Credentials = credentials;
}
+ // create recipients
if (null != targetProperties.Recipients) {
List<Recipient> recipients = new ArrayList<Recipient>(
targetProperties.Recipients.size());
@@ -244,7 +352,8 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
// set all other dispatch properties
notification.DispatchProperties = properties;
- NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(target.getNotificationType());
+ // dispatch
+ NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);
DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
m_executor.execute(runnable);
@@ -254,12 +363,74 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
/**
* {@inheritDoc}
* <p/>
- * Returns a schedule that starts after 1 minute and runs every minute after
- * {@link #runOneIteration()} completes.
+ * Returns a schedule that starts after 2 minute and runs every 2 minutes
+ * after {@link #runOneIteration()} completes.
*/
@Override
protected Scheduler scheduler() {
- return Scheduler.newFixedDelaySchedule(1, 1, TimeUnit.MINUTES);
+ return Scheduler.newFixedDelaySchedule(2, 2, TimeUnit.MINUTES);
+ }
+
+ /**
+ * 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.
+ *
+ * @param notification
+ * the notification (not {@code null}).
+ * @param histories
+ * the alerts to generate the content fron (not {@code null}.
+ * @param target
+ * the target of the {@link Notification}.
+ */
+ private void renderNotificationContent(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);
+ 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);
+
+ // 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
+ for (AlertHistoryEntity alert : histories) {
+ subjectWriter.write("Apache Ambari Alert Summary");
+ bodyWriter.write(alert.getAlertState().name());
+ bodyWriter.write(" ");
+ bodyWriter.write(alert.getAlertDefinition().getLabel());
+ bodyWriter.write(" ");
+ bodyWriter.write(alert.getAlertText());
+ bodyWriter.write("\n");
+ }
+ }
+
+ notification.Subject = subjectWriter.toString();
+ notification.Body = bodyWriter.toString();
}
/**
@@ -395,4 +566,398 @@ 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.
+ */
+ public final static class AlertInfo {
+ private int m_okCount = 0;
+ private int m_warningCount = 0;
+ private int m_criticalCount = 0;
+ private int m_unknownCount = 0;
+
+ /**
+ * The hosts that have at least 1 alert reported.
+ */
+ private final Set<String> m_hosts = new HashSet<String>();
+
+ /**
+ * The services that have at least 1 alert reported.
+ */
+ private final Set<String> m_services = new HashSet<String>();
+
+ /**
+ * All of the alerts for the {@link Notification}.
+ */
+ private final List<AlertHistoryEntity> m_alerts;
+
+ /**
+ * 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>>>();
+
+ /**
+ * A mapping of all services by state.
+ */
+ private final Map<String, Set<String>> m_servicesByState = new HashMap<String, Set<String>>();
+
+ /**
+ * A mapping of all alerts by the service that owns them.
+ */
+ private final Map<String, List<AlertHistoryEntity>> m_alertsByService = new HashMap<String, List<AlertHistoryEntity>>();
+
+ /**
+ * Constructor.
+ *
+ * @param histories
+ */
+ protected AlertInfo(List<AlertHistoryEntity> histories) {
+ m_alerts = histories;
+
+ // group all alerts by their service and severity
+ for (AlertHistoryEntity history : m_alerts) {
+ AlertState alertState = history.getAlertState();
+ String serviceName = history.getServiceName();
+ String hostName = history.getHostName();
+
+ if( null != hostName ){
+ m_hosts.add(hostName);
+ }
+
+ if (null != serviceName) {
+ m_services.add(serviceName);
+ }
+
+ // group alerts by service name & state
+ Map<AlertState, List<AlertHistoryEntity>> service = m_alertsByServiceAndState.get(serviceName);
+ if (null == service) {
+ service = new HashMap<AlertState, List<AlertHistoryEntity>>();
+ m_alertsByServiceAndState.put(serviceName, service);
+ }
+
+ List<AlertHistoryEntity> alertList = service.get(alertState);
+ if (null == alertList) {
+ alertList = new ArrayList<AlertHistoryEntity>();
+ service.put(alertState, alertList);
+ }
+
+ alertList.add(history);
+
+ // group services by alert states
+ Set<String> services = m_servicesByState.get(alertState.name());
+ if (null == services) {
+ services = new HashSet<String>();
+ m_servicesByState.put(alertState.name(), services);
+ }
+
+ services.add(serviceName);
+
+ // group alerts by service
+ List<AlertHistoryEntity> alertsByService = m_alertsByService.get(serviceName);
+ if (null == alertsByService) {
+ alertsByService = new ArrayList<AlertHistoryEntity>();
+ m_alertsByService.put(serviceName, alertsByService);
+ }
+
+ alertsByService.add(history);
+
+ // keep track of totals
+ switch (alertState) {
+ case CRITICAL:
+ m_criticalCount++;
+ break;
+ case OK:
+ m_okCount++;
+ break;
+ case UNKNOWN:
+ m_unknownCount++;
+ break;
+ case WARNING:
+ m_warningCount++;
+ break;
+ default:
+ m_unknownCount++;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the total number of OK alerts in the {@link Notification}.
+ *
+ * @return the OK count.
+ */
+ public int getOkCount() {
+ return m_okCount;
+ }
+
+ /**
+ * Gets the total number of WARNING alerts in the {@link Notification}.
+ *
+ * @return the WARNING count.
+ */
+ public int getWarningCount() {
+ return m_warningCount;
+ }
+
+ /**
+ * Gets the total number of CRITICAL alerts in the {@link Notification}.
+ *
+ * @return the CRITICAL count.
+ */
+ public int getCriticalCount() {
+ return m_criticalCount;
+ }
+
+ /**
+ * Gets the total number of UNKNOWN alerts in the {@link Notification}.
+ *
+ * @return the UNKNOWN count.
+ */
+ public int getUnknownCount() {
+ return m_unknownCount;
+ }
+
+ /**
+ * Gets the total count of all alerts in the {@link Notification}
+ *
+ * @return the total count of all alerts.
+ */
+ public int getTotalCount() {
+ return m_okCount + m_warningCount + m_criticalCount + m_unknownCount;
+ }
+
+ /**
+ * Gets all of the services that have alerts being reporting in this
+ * notification dispatch.
+ *
+ * @return the list of services.
+ */
+ public Set<String> getServices() {
+ return m_services;
+ }
+
+ /**
+ * Gets all of the alerts in the {@link Notification}.
+ *
+ * @return all of the alerts.
+ */
+ public List<AlertHistoryEntity> getAlerts() {
+ return m_alerts;
+ }
+
+ /**
+ * Gets all of the alerts in the {@link Notification} by service name.
+ *
+ * @param serviceName
+ * the service name.
+ * @return the alerts for that service, or {@code null} none.
+ */
+ public List<AlertHistoryEntity> getAlerts(String serviceName) {
+ return m_alertsByService.get(serviceName);
+ }
+
+ /**
+ * Gets all of the alerts for a given service and alert state level.
+ *
+ * @param serviceName
+ * the name of the service.
+ * @param alertState
+ * the alert state level.
+ * @return the list of alerts or {@code null} for none.
+ */
+ public List<AlertHistoryEntity> getAlerts(String serviceName,
+ String alertState) {
+
+ Map<AlertState, List<AlertHistoryEntity>> serviceAlerts = m_alertsByServiceAndState.get(serviceName);
+ if (null == serviceAlerts) {
+ return null;
+ }
+
+ AlertState state = AlertState.valueOf(alertState);
+ return serviceAlerts.get(state);
+ }
+
+ /**
+ * Gets a list of services that have an alert being reporting for the given
+ * state.
+ *
+ * @param alertState
+ * the state to get the services for.
+ * @return the services or {@code null} if none.
+ */
+ public Set<String> getServicesByAlertState(String alertState) {
+ return m_servicesByState.get(alertState);
+ }
+ }
+
+ /**
+ * The {@link AmbariInfo} class is used to provide the template engine with
+ * information about the Ambari installation.
+ */
+ public final static class AmbariInfo {
+ private String m_hostName = null;
+ private String m_url = null;
+ private String m_version = null;
+
+ /**
+ * Constructor.
+ *
+ * @param metaInfo
+ */
+ protected AmbariInfo(AmbariMetaInfo metaInfo) {
+ m_version = metaInfo.getServerVersion();
+ }
+
+ /**
+ * @return the hostName
+ */
+ public String getHostName() {
+ return m_hostName;
+ }
+
+ /**
+ * @return the url
+ */
+ public String getUrl() {
+ return m_url;
+ }
+
+ /**
+ * Gets the Ambari server version.
+ *
+ * @return the version
+ */
+ public String getServerVersion() {
+ return m_version;
+ }
+ }
+
+ /**
+ * The {@link DispatchInfo} class is used to provide the template engine with
+ * information about the intended target of the notification.
+ */
+ public static final class DispatchInfo {
+ private String m_targetName;
+ private String m_targetDescription;
+
+ /**
+ * Constructor.
+ *
+ * @param target
+ * the {@link AlertTargetEntity} receiving the notification.
+ */
+ protected DispatchInfo(AlertTargetEntity target) {
+ m_targetName = target.getTargetName();
+ m_targetDescription = target.getDescription();
+ }
+
+ /**
+ * Gets the name of the notification target.
+ *
+ * @return the name of the target.
+ */
+ public String getTargetName() {
+ return m_targetName;
+ }
+
+ /**
+ * Gets the description of the notification target.
+ *
+ * @return the target description.
+ */
+ public String getTargetDescription() {
+ return m_targetDescription;
+ }
+ }
+
+ /**
+ * The {@link AlertTemplates} class represnts the {@link AlertTemplates} that
+ * have been loaded, either by the {@link Configuration} or by the backup
+ * {@code alert-templates.xml} file.
+ */
+ @XmlRootElement(name = "alert-templates")
+ private final static class AlertTemplates {
+ /**
+ * The alert templates defined.
+ */
+ @XmlElement(name = "alert-template", required = true)
+ private List<AlertTemplate> m_templates;
+
+ /**
+ * Gets the alert template given the specified template type.
+ *
+ * @param type
+ * the template type.
+ * @return the template, or {@code null} if none.
+ * @see AlertTargetEntity#getNotificationType()
+ */
+ public AlertTemplate getTemplate(String type) {
+ for (AlertTemplate template : m_templates) {
+ if (type.equals(template.getType())) {
+ return template;
+ }
+ }
+
+ return null;
+ }
+ }
+
+ /**
+ * The {@link AlertTemplate} class represents a template for a specified alert
+ * target type that can be used when creating the content for dispatching
+ * {@link Notification}s.
+ */
+ private final static class AlertTemplate {
+ /**
+ * The type that this template is for.
+ *
+ * @see AlertTargetEntity#getNotificationType()
+ */
+ @XmlAttribute(name = "type", required = true)
+ private String m_type;
+
+ /**
+ * The subject template for the {@link Notification}.
+ */
+ @XmlElement(name = "subject", required = true)
+ private String m_subject;
+
+ /**
+ * The body template for the {@link Notification}.
+ */
+ @XmlElement(name = "body", required = true)
+ private String m_body;
+
+ /**
+ * Gets the template type.
+ *
+ * @return the template type.
+ */
+ public String getType() {
+ return m_type;
+ }
+
+ /**
+ * Gets the subject template.
+ *
+ * @return the subject template.
+ */
+ public String getSubject() {
+ return m_subject;
+ }
+
+ /**
+ * Gets the body template.
+ *
+ * @return the body template.
+ */
+ public String getBody() {
+ return m_body;
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/resources/alert-templates.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/alert-templates.xml b/ambari-server/src/main/resources/alert-templates.xml
new file mode 100644
index 0000000..16a2489
--- /dev/null
+++ b/ambari-server/src/main/resources/alert-templates.xml
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<alert-templates>
+ <alert-template type="EMAIL">
+ <subject>
+ <![CDATA[Alert Summary: OK[$summary.getOkCount()], Warning[$summary.getWarningCount()], Critical[$summary.getCriticalCount()], Unknown[$summary.getUnknownCount()]]]>
+ </subject>
+ <body>
+ <![CDATA[
+#set( $alertStates = ["OK", "WARNING", "CRITICAL", "UNKNOWN"] )
+#set( $services = $summary.getServices() )
+<html>
+ <style type="text/css">
+ .ok{
+ color: green;
+ }
+ .warning{
+ color: ffff00;
+ }
+ .critical{
+ color: red;
+ }
+ .unknown{
+ color: brown;
+ }
+ .section{
+ background-color: #eee;
+ padding: 20px;
+ margin-top: 10px;
+ margin-left: 20px;
+ margin-right: 20px;
+ text-align: left;
+ border-radius: 10px;
+ font-family: Arial, Helvetica, sans-serif;
+ }
+ .footer{
+ margin-top: 10px;
+ margin-left: 20px;
+ margin-right: 20px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 12px;
+ }
+ .alert{
+ margin: 5px;
+ font-weight: normal;
+ font-size: 14px;
+ }
+ .alertText{
+ font-size: 12px;
+ }
+ h1{
+ margin: 0 0 0.5em 0;
+ color: #343434;
+ font-weight: normal;
+ font-size: 20px;
+ }
+ table {
+ width: 100%;
+ }
+ tr{
+ margin-botton: 5px;
+ }
+ td{
+ text-align: left;
+ vertical-align: top;
+ }
+ </style>
+ <div class="section">
+ <h1>Services Reporting Alerts</h1>
+ #foreach( $alertState in $alertStates )
+ #if( $summary.getServicesByAlertState($alertState) )
+ <div class="alert">
+ <span class="$alertState">$alertState</span> $summary.getServicesByAlertState($alertState)
+ </div>
+ #end
+ #end
+ </div>
+
+ #foreach( $service in $services )
+ <div class="section">
+ <h1>$service</h1>
+ <table>
+ #foreach( $alertState in $alertStates )
+ #foreach( $alert in $summary.getAlerts($service,$alertState) )
+ <tr class="alert">
+ <td width="100px">
+ <div class="$alertState">
+ $alertState
+ </div>
+ </td>
+ <td>
+ $alert.getAlertDefinition().getLabel()
+ <br/>
+ <span class="alertText">
+ $alert.getAlertText()
+ <span>
+ </td>
+ </tr>
+ #end
+ #end
+ </table>
+ </div>
+ #end
+ <div class="footer">
+ This notification was sent to $dispatch.getTargetName()
+ <br/>
+ Apache Ambari $ambari.getServerVersion()
+ </div>
+</html>
+ ]]>
+ </body>
+ </alert-template>
+ <alert-template type="SNMP">
+ <subject>
+ <![CDATA[Alert Summary: OK[$summary.getOkCount()], Warning[$summary.getWarningCount()], Critical[$summary.getCriticalCount()], Unknown[$summary.getUnknownCount()]]]>
+ </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()]
+#end
+#end
+ ]]>
+ </body>
+ </alert-template>
+</alert-templates>
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json b/ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json
index 96cb931..f356426 100644
--- a/ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json
+++ b/ambari-server/src/main/resources/stacks/BIGTOP/0.8/services/HDFS/alerts.json
@@ -372,7 +372,7 @@
"DATANODE": [
{
"name": "datanode_process",
- "label": "DateNode Process",
+ "label": "DataNode Process",
"interval": 1,
"scope": "HOST",
"enabled": true,
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json b/ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json
index 11836b3..541c0e5 100644
--- a/ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json
+++ b/ambari-server/src/main/resources/stacks/HDP/1.3.2/services/HDFS/alerts.json
@@ -349,7 +349,7 @@
"DATANODE": [
{
"name": "datanode_process",
- "label": "DateNode Process",
+ "label": "DataNode Process",
"interval": 1,
"scope": "HOST",
"enabled": true,
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json b/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
index e58c4a3..595ae9c 100644
--- a/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
+++ b/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
@@ -386,8 +386,8 @@
"DATANODE": [
{
"name": "datanode_process",
- "label": "DateNode Process",
- "description": "Checks that the DateNode process responds to a TCP port request.",
+ "label": "DataNode Process",
+ "description": "Checks that the DataNode process responds to a TCP port request.",
"interval": 1,
"scope": "HOST",
"enabled": true,
http://git-wip-us.apache.org/repos/asf/ambari/blob/650e9d43/ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java
new file mode 100644
index 0000000..2e984bf
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/services/AlertNoticeDispatchServiceTest.java
@@ -0,0 +1,339 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ambari.server.state.services;
+
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.TimeZone;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+
+import org.apache.ambari.server.api.services.AmbariMetaInfo;
+import org.apache.ambari.server.notifications.DispatchFactory;
+import org.apache.ambari.server.notifications.Notification;
+import org.apache.ambari.server.notifications.NotificationDispatcher;
+import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
+import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
+import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
+import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
+import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
+import org.apache.ambari.server.orm.entities.AlertTargetEntity;
+import org.apache.ambari.server.state.AlertState;
+import org.apache.ambari.server.state.NotificationState;
+import org.apache.ambari.server.state.alert.Scope;
+import org.apache.ambari.server.state.alert.SourceType;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+
+/**
+ * Tests the {@link AlertNoticeDispatchService}.
+ */
+public class AlertNoticeDispatchServiceTest extends AlertNoticeDispatchService {
+
+ final static String ALERT_NOTICE_UUID = 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"));
+
+ private AmbariMetaInfo m_metaInfo = null;
+ private DispatchFactory m_dispatchFactory = null;
+ private AlertDispatchDAO m_dao = null;
+ private Injector m_injector;
+
+ List<AlertDefinitionEntity> m_definitions = new ArrayList<AlertDefinitionEntity>();
+ List<AlertHistoryEntity> m_histories = new ArrayList<AlertHistoryEntity>();
+
+ @Before
+ public void before() {
+ m_dao = createStrictMock(AlertDispatchDAO.class);
+ m_dispatchFactory = createStrictMock(DispatchFactory.class);
+ m_metaInfo = createNiceMock(AmbariMetaInfo.class);
+
+ // create an injector which will inject the mocks
+ m_injector = Guice.createInjector(Modules.override(
+ new InMemoryDefaultTestModule()).with(new MockModule()));
+
+ Assert.assertNotNull(m_injector);
+
+ // create 5 definitions
+ for (int i = 0; i < 5; i++) {
+ AlertDefinitionEntity definition = new AlertDefinitionEntity();
+ definition.setDefinitionName("Alert Definition " + i);
+ definition.setServiceName("Service " + i);
+ definition.setComponentName(null);
+ definition.setClusterId(1L);
+ definition.setHash(UUID.randomUUID().toString());
+ definition.setScheduleInterval(Integer.valueOf(60));
+ definition.setScope(Scope.SERVICE);
+ definition.setSource("{\"type\" : \"SCRIPT\"}");
+ definition.setSourceType(SourceType.SCRIPT);
+
+ m_definitions.add(definition);
+ }
+
+
+ // create 10 historical alerts for each definition, 8 OK and 2 CRIT
+ calendar.clear();
+ calendar.set(2014, Calendar.JANUARY, 1);
+
+ for (AlertDefinitionEntity definition : m_definitions) {
+ for (int i = 0; i < 10; i++) {
+ AlertHistoryEntity history = new AlertHistoryEntity();
+ history.setServiceName(definition.getServiceName());
+ history.setClusterId(1L);
+ history.setAlertDefinition(definition);
+ history.setAlertLabel(definition.getDefinitionName() + " " + i);
+ history.setAlertText(definition.getDefinitionName() + " " + i);
+ history.setAlertTimestamp(calendar.getTimeInMillis());
+ history.setHostName(HOSTNAME);
+
+ history.setAlertState(AlertState.OK);
+ if (i == 0 || i == 5) {
+ history.setAlertState(AlertState.CRITICAL);
+ }
+
+ // increase the days for each
+ calendar.add(Calendar.DATE, 1);
+ m_histories.add(history);
+ }
+ }
+ }
+
+ /**
+ * Tests the parsing of the {@link AlertHistoryEntity} list into
+ * {@link AlertInfo}.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testAlertInfo() throws Exception {
+ AlertInfo alertInfo = new AlertInfo(m_histories);
+ assertEquals(50, alertInfo.getAlerts().size());
+ assertEquals(10, alertInfo.getAlerts("Service 1").size());
+ assertEquals(10, alertInfo.getAlerts("Service 2").size());
+
+ assertEquals(8, alertInfo.getAlerts("Service 1", "OK").size());
+ assertEquals(2, alertInfo.getAlerts("Service 1", "CRITICAL").size());
+ assertNull(alertInfo.getAlerts("Service 1", "WARNING"));
+ assertNull(alertInfo.getAlerts("Service 1", "UNKNOWN"));
+
+ assertEquals(5, alertInfo.getServices().size());
+ }
+
+ /**
+ * Tests that the dispatcher is not called when there are no notices.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testNoDispatch() throws Exception {
+ EasyMock.expect(m_dao.findPendingNotices()).andReturn(
+ new ArrayList<AlertNoticeEntity>()).once();
+
+ // m_dispatchFactory should not be called at all
+ 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
+ service.runOneIteration();
+
+ EasyMock.verify(m_dao, m_dispatchFactory);
+ }
+
+ /**
+ * Tests that the dispatcher is not called when there are no notices.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testDispatch() throws Exception {
+ MockDispatcher dispatcher = new MockDispatcher();
+
+ EasyMock.expect(m_dao.findPendingNotices()).andReturn(getMockNotices()).once();
+
+ EasyMock.expect(m_dispatchFactory.getDispatcher("EMAIL")).andReturn(
+ dispatcher).once();
+
+ 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);
+
+ Notification notification = dispatcher.getNotification();
+ assertNotNull(notification);
+
+ assertTrue(notification.Subject.contains("OK[1]"));
+ assertTrue(notification.Subject.contains("Critical[0]"));
+ assertTrue(notification.Body.contains(ALERT_UNIQUE_TEXT));
+ }
+
+ /**
+ * Tests that a failed dispatch invokes the callback to mark the UUIDs of the
+ * notices as FAILED.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testFailedDispatch() throws Exception {
+ MockDispatcher dispatcher = new MockDispatcher();
+ List<AlertNoticeEntity> notices = getMockNotices();
+ 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.replay(m_dao, m_dispatchFactory);
+
+ // do NOT startup the service which will force a template NPE
+ AlertNoticeDispatchService service = m_injector.getInstance(AlertNoticeDispatchService.class);
+
+ // service trigger with mock executor that blocks
+ service.setExecutor(new MockExecutor());
+ service.runOneIteration();
+
+ EasyMock.verify(m_dao, m_dispatchFactory);
+
+ Notification notification = dispatcher.getNotification();
+ assertNull(notification);
+ }
+
+ /**
+ * Gets PENDING notices.
+ *
+ * @return
+ */
+ private List<AlertNoticeEntity> getMockNotices(){
+ AlertHistoryEntity history = new AlertHistoryEntity();
+ history.setServiceName("HDFS");
+ history.setClusterId(1L);
+ history.setAlertDefinition(null);
+ history.setAlertLabel("Label");
+ history.setAlertState(AlertState.OK);
+ history.setAlertText(ALERT_UNIQUE_TEXT);
+ history.setAlertTimestamp(System.currentTimeMillis());
+
+ AlertTargetEntity target = new AlertTargetEntity();
+ target.setAlertStates(EnumSet.allOf(AlertState.class));
+ target.setTargetName("Alert Target");
+ target.setDescription("Mock Target");
+ target.setNotificationType("EMAIL");
+
+ String properties = "{ \"foo\" : \"bar\" }";
+ target.setProperties(properties);
+
+ AlertNoticeEntity notice = new AlertNoticeEntity();
+ notice.setUuid(ALERT_NOTICE_UUID);
+ notice.setAlertTarget(target);
+ notice.setAlertHistory(history);
+ notice.setNotifyState(NotificationState.PENDING);
+
+ ArrayList<AlertNoticeEntity> notices = new ArrayList<AlertNoticeEntity>();
+ notices.add(notice);
+
+ return notices;
+ }
+
+ /**
+ * A mock dispatcher that captures the {@link Notification}.
+ */
+ private static final class MockDispatcher implements NotificationDispatcher {
+
+ private Notification m_notificaiton;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getType() {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dispatch(Notification notification) {
+ m_notificaiton = notification;
+ }
+
+ public Notification getNotification() {
+ return m_notificaiton;
+ }
+ }
+
+ /**
+ * An {@link Executor} that calls {@link Runnable#run()} directly in the
+ * current thread.
+ */
+ private static final class MockExecutor implements Executor {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ }
+
+ /**
+ *
+ */
+ private class MockModule implements Module {
+ /**
+ *
+ */
+ @Override
+ public void configure(Binder binder) {
+ binder.bind(AlertDispatchDAO.class).toInstance(m_dao);
+ binder.bind(DispatchFactory.class).toInstance(m_dispatchFactory);
+ binder.bind(AmbariMetaInfo.class).toInstance(m_metaInfo);
+
+ EasyMock.expect(m_metaInfo.getServerVersion()).andReturn("2.0.0").anyTimes();
+ }
+ }
+}