This is an automated email from the ASF dual-hosted git repository.
akshayrai09 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 31d916f [TE] Adding Entity GroupKey Anomalies Email Reporter (#4433)
31d916f is described below
commit 31d916ff6563d7cac38eac0d0b9d824825bfacf0
Author: Akshay Rai <[email protected]>
AuthorDate: Wed Jul 17 08:30:47 2019 -0700
[TE] Adding Entity GroupKey Anomalies Email Reporter (#4433)
Includes a new template to send out alert emails for entity level anomalies
with groupKey.
---
.../alert/content/BaseEmailContentFormatter.java | 87 ++++++++----
.../content/EntityGroupByContentFormatter.java | 147 +++++++++++++++++++++
...HierarchicalAnomaliesEmailContentFormatter.java | 1 +
.../MetricAnomaliesEmailContentFormatter.java | 2 +
...nboardingNotificationEmailContentFormatter.java | 1 +
.../dashboard/resources/v2/AnomaliesResource.java | 24 ++--
.../thirdeye/detection/DefaultDataProvider.java | 2 +-
.../alert/scheme/DetectionEmailAlerter.java | 4 +-
.../wrapper/ChildKeepingMergeWrapper.java | 16 ++-
.../thirdeye/detection/wrapper/GrouperWrapper.java | 2 +-
.../detector/entity-groupby-anomaly-report.ftl | 97 ++++++++++++++
.../alert/content/ContentFormatterUtils.java | 48 +++++++
...=> TestEntityGroupByEmailContentFormatter.java} | 136 ++++++++++---------
...HierarchicalAnomaliesEmailContentFormatter.java | 20 +--
.../TestMetricAnomaliesEmailContentFormatter.java | 20 +--
...TestOnboardingNotificationContentFormatter.java | 20 +--
.../pinot/thirdeye/datalayer/DaoTestUtils.java | 13 ++
.../thirdeye/detection/yaml/YamlResourceTest.java | 2 +-
.../tools/RunAdhocDatabaseQueriesTool.java | 33 ++++-
.../detection/yaml/alertconfig/alert-config-5.yaml | 2 +-
...est-entity-groupby-email-content-formatter.html | 105 +++++++++++++++
21 files changed, 619 insertions(+), 163 deletions(-)
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
index 79600b7..dd06e34 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/BaseEmailContentFormatter.java
@@ -190,42 +190,46 @@ public abstract class BaseEmailContentFormatter
implements EmailContentFormatter
protected abstract void updateTemplateDataByAnomalyResults(Map<String,
Object> templateData,
Collection<AnomalyResult> anomalies, EmailContentFormatterContext
context);
- /**
- * Add the auxiliary email information into parameter map
- * @param alertConfigDTO
- * @param groupId
- * @param groupName
- * @param anomalies
- * @return
- */
- protected Map<String, Object> getTemplateData(AlertConfigDTO alertConfigDTO,
Long groupId, String groupName,
- Collection<AnomalyResult> anomalies) {
- Map<String, Object> templateData = new HashMap<>();
-
- DateTimeZone timeZone =
DateTimeZone.forTimeZone(AlertTaskRunnerV2.DEFAULT_TIME_ZONE);
-
+ protected void enrichMetricInfo(Map<String, Object> templateData,
Collection<AnomalyResult> anomalies) {
Set<String> metrics = new TreeSet<>();
Set<String> datasets = new TreeSet<>();
- List<MergedAnomalyResultDTO> mergedAnomalyResults = new ArrayList<>();
Map<String, MetricConfigDTO> metricsMap = new TreeMap<>();
-
- // Calculate start and end time of the anomalies
- DateTime startTime = DateTime.now();
- DateTime endTime = new DateTime(0l);
for (AnomalyResult anomalyResult : anomalies) {
if (anomalyResult instanceof MergedAnomalyResultDTO) {
MergedAnomalyResultDTO mergedAnomaly = (MergedAnomalyResultDTO)
anomalyResult;
- mergedAnomalyResults.add(mergedAnomaly);
datasets.add(mergedAnomaly.getCollection());
metrics.add(mergedAnomaly.getMetric());
MetricConfigDTO metric =
this.metricDAO.findByMetricAndDataset(mergedAnomaly.getMetric(),
mergedAnomaly.getCollection());
if (metric != null) {
- // NOTE: our stale freemarker version doesn't play nice with
non-string keys
metricsMap.put(metric.getId().toString(), metric);
}
}
+ }
+
+ templateData.put("datasetsCount", datasets.size());
+ templateData.put("datasets", StringUtils.join(datasets, ", "));
+ templateData.put("metricsCount", metrics.size());
+ templateData.put("metrics", StringUtils.join(metrics, ", "));
+ templateData.put("metricsMap", metricsMap);
+ }
+
+ protected Map<String, Object> getTemplateData(AlertConfigDTO alertConfigDTO,
Long groupId, String groupName,
+ Collection<AnomalyResult> anomalies) {
+ Map<String, Object> templateData = new HashMap<>();
+
+ DateTimeZone timeZone =
DateTimeZone.forTimeZone(TimeZone.getTimeZone(DEFAULT_TIME_ZONE));
+ List<MergedAnomalyResultDTO> mergedAnomalyResults = new ArrayList<>();
+
+ // Calculate start and end time of the anomalies
+ DateTime startTime = DateTime.now();
+ DateTime endTime = new DateTime(0l);
+ for (AnomalyResult anomalyResult : anomalies) {
+ if (anomalyResult instanceof MergedAnomalyResultDTO) {
+ MergedAnomalyResultDTO mergedAnomaly = (MergedAnomalyResultDTO)
anomalyResult;
+ mergedAnomalyResults.add(mergedAnomaly);
+ }
if (anomalyResult.getStartTime() < startTime.getMillis()) {
startTime = new DateTime(anomalyResult.getStartTime(), dateTimeZone);
}
@@ -236,11 +240,6 @@ public abstract class BaseEmailContentFormatter implements
EmailContentFormatter
PrecisionRecallEvaluator precisionRecallEvaluator = new
PrecisionRecallEvaluator(new DummyAlertFilter(), mergedAnomalyResults);
- templateData.put("datasetsCount", datasets.size());
- templateData.put("datasets", StringUtils.join(datasets, ", "));
- templateData.put("metricsCount", metrics.size());
- templateData.put("metrics", StringUtils.join(metrics, ", "));
- templateData.put("metricsMap", metricsMap);
templateData.put("anomalyCount", anomalies.size());
templateData.put("startTime", getDateString(startTime));
templateData.put("endTime", getDateString(endTime));
@@ -592,6 +591,10 @@ public abstract class BaseEmailContentFormatter implements
EmailContentFormatter
String endTime;
String timezone;
String issueType;
+ String score;
+ Double weight;
+ String groupKey;
+ String entityName;
private static String RAW_VALUE_FORMAT = "%.0f";
private static String PERCENTAGE_FORMAT = "%.2f %%";
@@ -793,6 +796,38 @@ public abstract class BaseEmailContentFormatter implements
EmailContentFormatter
this.issueType = issueType;
}
+ public void setWeight(Double weight) {
+ this.weight = weight;
+ }
+
+ public double getWeight() {
+ return weight;
+ }
+
+ public String getScore() {
+ return score;
+ }
+
+ public void setScore(String score) {
+ this.score = score;
+ }
+
+ public String getEntityName() {
+ return entityName;
+ }
+
+ public void setEntityName(String entityName) {
+ this.entityName = entityName;
+ }
+
+ public String getGroupKey() {
+ return groupKey;
+ }
+
+ public void setGroupKey(String groupKey) {
+ this.groupKey = groupKey;
+ }
+
public boolean isPositiveWoWLift() {
return positiveWoWLift;
}
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
new file mode 100644
index 0000000..a20ed2a
--- /dev/null
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/EntityGroupByContentFormatter.java
@@ -0,0 +1,147 @@
+/*
+ * 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.pinot.thirdeye.alert.content;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.pinot.thirdeye.anomalydetection.context.AnomalyResult;
+import org.apache.pinot.thirdeye.datalayer.bao.DetectionConfigManager;
+import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
+import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
+import org.apache.pinot.thirdeye.datasource.DAORegistry;
+import org.apache.pinot.thirdeye.util.ThirdEyeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * This email formatter generates a report/alert from the anomalies having a
groupKey
+ */
+public class EntityGroupByContentFormatter extends BaseEmailContentFormatter{
+ private static final Logger LOG =
LoggerFactory.getLogger(EntityGroupByContentFormatter.class);
+
+ private static final String EMAIL_TEMPLATE = "emailTemplate";
+ private static final String DEFAULT_EMAIL_TEMPLATE =
"entity-groupby-anomaly-report.ftl";
+
+ static final String PROP_ENTITY_NAME = "entityName";
+ static final String PROP_ANOMALY_SCORE = "groupScore";
+ static final String PROP_GROUP_KEY = "groupKey";
+
+ private DetectionConfigManager configDAO = null;
+ private Map<String, Double> entityAnomalyToScoreMap = new HashMap<>();
+ private Multimap<String, AnomalyReportEntity> entityAnomalyReports =
ArrayListMultimap.create();
+ private Map<String, String> entityToAnomalyIdsMap = new HashMap<>();
+
+ public EntityGroupByContentFormatter() {}
+
+ @Override
+ public void init(Properties properties, EmailContentFormatterConfiguration
configuration) {
+ super.init(properties, configuration);
+ this.emailTemplate = properties.getProperty(EMAIL_TEMPLATE,
DEFAULT_EMAIL_TEMPLATE);
+ this.configDAO = DAORegistry.getInstance().getDetectionConfigManager();
+ }
+
+ @Override
+ protected void updateTemplateDataByAnomalyResults(Map<String, Object>
templateData,
+ Collection<AnomalyResult> anomalies, EmailContentFormatterContext
context) {
+ DetectionConfigDTO config = null;
+ Preconditions.checkArgument(anomalies != null && !anomalies.isEmpty(),
"Report has empty anomalies");
+ for (AnomalyResult anomalyResult : anomalies) {
+ if (!(anomalyResult instanceof MergedAnomalyResultDTO)) {
+ LOG.warn("Anomaly result {} isn't an instance of
MergedAnomalyResultDTO. Skip from alert.", anomalyResult);
+ continue;
+ }
+
+ MergedAnomalyResultDTO anomaly = (MergedAnomalyResultDTO) anomalyResult;
+ if (config == null) {
+ config = this.configDAO.findById(anomaly.getDetectionConfigId());
+ Preconditions.checkNotNull(config, String.format("Cannot find
detection config %d", anomaly.getDetectionConfigId()));
+ }
+
+ updateEntityToAnomalyDetailsMap(anomaly, config);
+ }
+
+ List<Map.Entry<String, Double>> anomaliesSortedByScores = new
ArrayList<>(entityAnomalyToScoreMap.entrySet());
+ anomaliesSortedByScores.sort(Map.Entry.comparingByValue());
+ List<String> entityList =
anomaliesSortedByScores.stream().map(Map.Entry::getKey).collect(Collectors.toList());
+ entityList.sort(Collections.reverseOrder());
+
+ templateData.put("emailHeading", config.getName());
+ templateData.put("entityToAnomalyIdsMap", entityToAnomalyIdsMap);
+ templateData.put("entitySortedByScoreList", entityList);
+ templateData.put("entityToAnomalyDetailsMap",
entityAnomalyReports.asMap());
+ }
+
+ /**
+ * Recursively find the anomalies having a groupKey and display them in the
email
+ */
+ private void updateEntityToAnomalyDetailsMap(MergedAnomalyResultDTO anomaly,
DetectionConfigDTO detectionConfig) {
+ if (anomaly.getProperties() != null &&
anomaly.getProperties().containsKey(PROP_GROUP_KEY)) {
+ AnomalyReportEntity anomalyReport = new
AnomalyReportEntity(String.valueOf(anomaly.getId()),
+ getAnomalyURL(anomaly,
emailContentFormatterConfiguration.getDashboardHost()),
+ ThirdEyeUtils.getRoundedValue(anomaly.getAvgBaselineVal()),
+ ThirdEyeUtils.getRoundedValue(anomaly.getAvgCurrentVal()), 0d, null,
+ getTimeDiffInHours(anomaly.getStartTime(), anomaly.getEndTime()),
getFeedbackValue(anomaly.getFeedback()),
+ detectionConfig.getName(), detectionConfig.getDescription(),
anomaly.getMetric(),
+ getDateString(anomaly.getStartTime(), dateTimeZone),
getDateString(anomaly.getEndTime(), dateTimeZone),
+ getTimezoneString(dateTimeZone), getIssueType(anomaly));
+
+ // Criticality score ranges from [0 to infinity)
+ double score = -1;
+ if (anomaly.getProperties().containsKey(PROP_ANOMALY_SCORE)) {
+ score =
Double.parseDouble(anomaly.getProperties().get(PROP_ANOMALY_SCORE));
+ anomalyReport.setScore(ThirdEyeUtils.getRoundedValue(score));
+ } else {
+ anomalyReport.setScore("-");
+ }
+ anomalyReport.setWeight(anomaly.getWeight());
+ anomalyReport.setGroupKey(anomaly.getProperties().get(PROP_GROUP_KEY));
+
anomalyReport.setEntityName(anomaly.getProperties().getOrDefault(PROP_ENTITY_NAME,
"UNKNOWN_ENTITY"));
+
+ Set<Long> childIds = new HashSet<>();
+ if (anomaly.getChildIds() != null) {
+ childIds.addAll(anomaly.getChildIds());
+ }
+
+ // include notified alerts only in the email
+ if (!includeSentAnomaliesOnly || anomaly.isNotified()) {
+
entityToAnomalyIdsMap.put(anomaly.getProperties().get(PROP_ENTITY_NAME),
Joiner.on(",").join(childIds));
+
entityAnomalyToScoreMap.put(anomaly.getProperties().get(PROP_ENTITY_NAME),
score);
+
entityAnomalyReports.put(anomaly.getProperties().get(PROP_ENTITY_NAME),
anomalyReport);
+ }
+ } else {
+ for (MergedAnomalyResultDTO childAnomaly : anomaly.getChildren()) {
+ updateEntityToAnomalyDetailsMap(childAnomaly, detectionConfig);
+ }
+ }
+ }
+}
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
index 5c69d06..fd79eb9 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/HierarchicalAnomaliesEmailContentFormatter.java
@@ -82,6 +82,7 @@ public class HierarchicalAnomaliesEmailContentFormatter
extends BaseEmailContent
@Override
protected void updateTemplateDataByAnomalyResults(Map<String, Object>
templateData,
Collection<AnomalyResult> anomalies, EmailContentFormatterContext
context) {
+ enrichMetricInfo(templateData, anomalies);
List<AnomalyReportEntity> rootAnomalyDetails = new ArrayList<>();
SortedMap<String, List<AnomalyReportEntity>> leafAnomalyDetails = new
TreeMap<>();
List<String> anomalyIds = new ArrayList<>();
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
index 385fe95..51d8610 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/MetricAnomaliesEmailContentFormatter.java
@@ -71,6 +71,8 @@ public class MetricAnomaliesEmailContentFormatter extends
BaseEmailContentFormat
@Override
protected void updateTemplateDataByAnomalyResults(Map<String, Object>
templateData,
Collection<AnomalyResult> anomalies, EmailContentFormatterContext
context) {
+ enrichMetricInfo(templateData, anomalies);
+
DateTime windowStart = DateTime.now();
DateTime windowEnd = new DateTime(0);
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
index a2bcb30..1cc1a50 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/alert/content/OnboardingNotificationEmailContentFormatter.java
@@ -59,6 +59,7 @@ public class OnboardingNotificationEmailContentFormatter
extends BaseEmailConten
@Override
protected void updateTemplateDataByAnomalyResults(Map<String, Object>
templateData,
Collection<AnomalyResult> anomalies, EmailContentFormatterContext
context) {
+ enrichMetricInfo(templateData, anomalies);
AnomalyFunctionDTO anomalyFunctionSpec = context.getAnomalyFunctionSpec();
for (AnomalyResult anomalyResult : anomalies) {
if (!(anomalyResult instanceof MergedAnomalyResultDTO)) {
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
index 6344d49..984d482 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/dashboard/resources/v2/AnomaliesResource.java
@@ -95,6 +95,7 @@ import
org.apache.pinot.thirdeye.datalayer.dto.GroupedAnomalyResultsDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
import org.apache.pinot.thirdeye.datalayer.pojo.AlertConfigBean;
+import org.apache.pinot.thirdeye.datalayer.pojo.MergedAnomalyResultBean;
import org.apache.pinot.thirdeye.datasource.DAORegistry;
import org.apache.pinot.thirdeye.datasource.ThirdEyeCacheRegistry;
import org.apache.pinot.thirdeye.datasource.cache.QueryCache;
@@ -303,7 +304,7 @@ public class AnomaliesResource {
List<MergedAnomalyResultDTO> mergedAnomalies =
mergedAnomalyResultDAO.findByTime(startTime, endTime);
AnomaliesWrapper anomaliesWrapper =
- constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies,
searchFiltersJSON, pageNumber, filterOnly);
+
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies),
searchFiltersJSON, pageNumber, filterOnly);
return anomaliesWrapper;
}
@@ -312,7 +313,6 @@ public class AnomaliesResource {
* @param startTime
* @param endTime
* @param anomalyIdsString
- * @param functionName
* @return
* @throws Exception
*/
@@ -323,7 +323,6 @@ public class AnomaliesResource {
@PathParam("endTime") Long endTime,
@PathParam("pageNumber") int pageNumber,
@QueryParam("anomalyIds") String anomalyIdsString,
- @QueryParam("functionName") String functionName,
@QueryParam("searchFilters") String searchFiltersJSON,
@QueryParam("filterOnly") @DefaultValue("false") boolean filterOnly)
throws Exception {
@@ -369,7 +368,7 @@ public class AnomaliesResource {
}
List<MergedAnomalyResultDTO> mergedAnomalies =
getAnomaliesForMetricIdsInRange(metricIds, startTime, endTime);
AnomaliesWrapper
- anomaliesWrapper =
constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies,
searchFiltersJSON, pageNumber, filterOnly);
+ anomaliesWrapper =
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies),
searchFiltersJSON, pageNumber, filterOnly);
return anomaliesWrapper;
}
@@ -419,7 +418,7 @@ public class AnomaliesResource {
}
AnomaliesWrapper
- anomaliesWrapper =
constructAnomaliesWrapperFromMergedAnomalies(mergedAnomalies,
searchFiltersJSON, pageNumber, filterOnly);
+ anomaliesWrapper =
constructAnomaliesWrapperFromMergedAnomalies(removeChildren(mergedAnomalies),
searchFiltersJSON, pageNumber, filterOnly);
return anomaliesWrapper;
}
@@ -680,6 +679,13 @@ public class AnomaliesResource {
return endDateTime.minus(periodToSubtract).getMillis();
}
+ /**
+ * Removes child anomalies
+ */
+ private List<MergedAnomalyResultDTO>
removeChildren(List<MergedAnomalyResultDTO> mergedAnomalies) {
+ mergedAnomalies.removeIf(MergedAnomalyResultBean::isChild);
+ return mergedAnomalies;
+ }
/**
* Constructs AnomaliesWrapper object from a list of merged anomalies
@@ -689,14 +695,6 @@ public class AnomaliesResource {
AnomaliesWrapper anomaliesWrapper = new AnomaliesWrapper();
- // remove child anomalies
- Iterator<MergedAnomalyResultDTO> itAnomaly = mergedAnomalies.iterator();
- while (itAnomaly.hasNext()) {
- if (itAnomaly.next().isChild()) {
- itAnomaly.remove();
- }
- }
-
//filter the anomalies
SearchFilters searchFilters = new SearchFilters();
if (searchFiltersJSON != null) {
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
index 4d890bf..fedd27f 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/DefaultDataProvider.java
@@ -248,7 +248,7 @@ public class DefaultDataProvider implements DataProvider {
// filter all child anomalies. those are kept in the parent anomaly
children set.
anomalies = Collections2.filter(anomalies, mergedAnomaly ->
mergedAnomaly != null && !mergedAnomaly.isChild());
- //LOG.info("Fetched {} anomalies between (startTime = {}, endTime = {})
with confid Id = {}", anomalies.size(), slice.getStart(), slice.getEnd(),
configId);
+ LOG.info("Fetched {} anomalies between (startTime = {}, endTime = {})
with confid Id = {}", anomalies.size(), slice.getStart(), slice.getEnd(),
configId);
output.putAll(slice, anomalies);
}
return output;
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
index 1f963df..e059a4e 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/alert/scheme/DetectionEmailAlerter.java
@@ -131,7 +131,7 @@ public class DetectionEmailAlerter extends
DetectionAlertScheme {
public enum EmailTemplate {
DEFAULT_EMAIL,
- ENTITY_REPORT
+ ENTITY_GROUPBY_REPORT
}
/**
@@ -148,7 +148,7 @@ public class DetectionEmailAlerter extends
DetectionAlertScheme {
LOG.info("Using the " + DEFAULT_EMAIL_FORMATTER_TYPE + " email
template.");
return
EmailContentFormatterFactory.fromClassName(DEFAULT_EMAIL_FORMATTER_TYPE);
- case ENTITY_REPORT:
+ case ENTITY_GROUPBY_REPORT:
LOG.info("Using the " + template + " email template.");
return
EmailContentFormatterFactory.fromClassName(ENTITY_REPORT_FORMATTER_TYPE);
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
index f3fa13c..89438aa 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/ChildKeepingMergeWrapper.java
@@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.detection.DataProvider;
@@ -39,6 +40,8 @@ import
org.apache.pinot.thirdeye.detection.algorithm.MergeWrapper;
* Merge anomalies regardless of anomaly merge key.
*/
public class ChildKeepingMergeWrapper extends BaselineFillingMergeWrapper {
+ private static final String PROP_GROUP_KEY = "groupKey";
+
public ChildKeepingMergeWrapper(DataProvider provider, DetectionConfigDTO
config, long startTime, long endTime) {
super(provider, config, startTime, endTime);
}
@@ -69,8 +72,14 @@ public class ChildKeepingMergeWrapper extends
BaselineFillingMergeWrapper {
continue;
}
+ // Prevent merging of grouped anomalies
+ String groupKey = "";
+ if (anomaly.getProperties().containsKey(PROP_GROUP_KEY)) {
+ groupKey = anomaly.getProperties().get(PROP_GROUP_KEY);
+ }
+
MergeWrapper.AnomalyKey key =
- new MergeWrapper.AnomalyKey(anomaly.getMetric(),
anomaly.getCollection(), anomaly.getDimensions(), "", "");
+ new MergeWrapper.AnomalyKey(anomaly.getMetric(),
anomaly.getCollection(), anomaly.getDimensions(), groupKey, "");
MergedAnomalyResultDTO parent = parents.get(key);
if (parent == null || anomaly.getStartTime() - parent.getEndTime() >
this.maxGap) {
@@ -97,10 +106,11 @@ public class ChildKeepingMergeWrapper extends
BaselineFillingMergeWrapper {
}
}
- // refill current and baseline values for qualified parent anomalies
+ // Refill current and baseline values for qualified parent anomalies
+ // Ignore filling baselines for exiting parent anomalies and grouped
anomalies
Collection<MergedAnomalyResultDTO> parentAnomalies =
Collections2.filter(output,
mergedAnomaly -> mergedAnomaly != null &&
!mergedAnomaly.getChildren().isEmpty() && !isExistingAnomaly(
- existingParentAnomalies, mergedAnomaly));
+ existingParentAnomalies, mergedAnomaly) &&
!StringUtils.isBlank(mergedAnomaly.getMetric()));
super.fillCurrentAndBaselineValue(new ArrayList<>(parentAnomalies));
return output;
}
diff --git
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
index 7d38bba..cfffd14 100644
---
a/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
+++
b/thirdeye/thirdeye-pinot/src/main/java/org/apache/pinot/thirdeye/detection/wrapper/GrouperWrapper.java
@@ -67,9 +67,9 @@ public class GrouperWrapper extends DetectionPipeline {
this.grouperName =
DetectionUtils.getComponentKey(MapUtils.getString(config.getProperties(),
PROP_GROUPER));
Preconditions.checkArgument(this.config.getComponents().containsKey(this.grouperName));
this.grouper = (Grouper) this.config.getComponents().get(this.grouperName);
+
Preconditions.checkArgument(this.config.getProperties().containsKey(PROP_ENTITY_NAME));
this.entityName =
this.config.getProperties().get(PROP_ENTITY_NAME).toString();
-
}
/**
diff --git
a/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
b/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
new file mode 100644
index 0000000..97a06ee
--- /dev/null
+++
b/thirdeye/thirdeye-pinot/src/main/resources/org/apache/pinot/thirdeye/detector/entity-groupby-anomaly-report.ftl
@@ -0,0 +1,97 @@
+<#import "lib/utils.ftl" as utils>
+
+<head>
+ <link href="https://fonts.googleapis.com/css?family=Open+Sans"
rel="stylesheet">
+</head>
+
+<body style="background-color: #EDF0F3;">
+<table border="0" cellpadding="0" cellspacing="0" style="width:100%;
font-family: 'Proxima Nova','Arial','Helvetica Neue',Helvetica,sans-serif;
font-size:14px; margin:0 auto; max-width: 50%; min-width: 700px;
background-color: #F3F6F8;">
+ <!-- White bar on top -->
+ <tr>
+ <td align="center" style="padding: 6px 24px;">
+ <span style="color: red; font-size: 35px; vertical-align:
middle;"> ⚠ </span>
+ <span style="color: rgba(0,0,0,0.75); font-size: 18px; font-weight:
bold; letter-spacing: 2px; vertical-align: middle;">ACTION REQUIRED ON THIRDEYE
ALERT</span>
+ </td>
+ </tr>
+
+ <!-- Blue header on top -->
+ <tr>
+ <td style="font-size: 16px; padding: 12px; background-color: #0073B1;
color: #FFF; text-align: center;">
+ <span style="font-size: 18px; font-weight: bold; letter-spacing: 2px;
vertical-align: middle;">Report: ${emailHeading}</span>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <table border="0" cellpadding="0" cellspacing="0" style="border:1px
solid #E9E9E9; border-radius: 2px; width: 100%;">
+
+ <!-- List all the alerts -->
+ <#list entitySortedByScoreList as entity>
+ <@utils.addBlock title="" align="left">
+ <!-- Display Entity Name -->
+ <p>
+ <span style="color: #1D1D1D; font-size: 20px; font-weight: bold;
display:inline-block; vertical-align: middle;">Entity: </span>
+ <span style="color: #606060; font-size: 20px; text-decoration:
none; display:inline-block; vertical-align: middle; width: 70%; white-space:
nowrap; overflow: hidden; text-overflow: ellipsis;">${entity}</span>
+ </p>
+
+ <!-- List all the anomalies under this entity in a table -->
+ <table border="0" width="100%" align="center" style="width:100%;
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+ <tr style="text-align:center; background-color: #F6F8FA;
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+ <th style="text-align:left; padding: 6px 12px; font-size:
12px; font-weight: bold; line-height: 20px;">Feature</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Criticality Score*</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Current</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Predicted</th>
+ </tr>
+
+ <#list entityToAnomalyDetailsMap[entity] as anomaly>
+ <tr style="border-bottom: 1px solid #C7D1D8;">
+ <td style="padding: 6px 12px;white-space: nowrap;">
+ <a style="font-weight: bold; text-decoration: none;
font-size:14px; line-height:20px; color: #0073B1;"
href="${dashboardHost}/app/#/anomalies?anomalyIds=${entityToAnomalyIdsMap[entity]}"
+ target="_blank">${anomaly.groupKey}</a>
+ <span style="color: rgba(0,0,0,0.6); font-size:12px;
line-height:16px;">${anomaly.duration}</span>
+ </td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">${anomaly.score}</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">${anomaly.currentVal}</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">
+ ${anomaly.baselineVal}
+ <div style="font-size: 12px;
color:${anomaly.positiveLift?string('#3A8C18','#ee1620')};">(${anomaly.positiveLift?string('+','')}${anomaly.lift})</div>
+ </td>
+ </tr>
+ </#list>
+ </table>
+
+ </@utils.addBlock>
+ </#list>
+
+ <!-- Reference Links -->
+ <#if referenceLinks?has_content>
+ <@utils.addBlock title="Useful Links" align="left">
+ <table border="0" align="center" style="table-layout: fixed;
width:100%; padding:0; margin:0; border-collapse: collapse; text-align:left;">
+ <#list referenceLinks?keys as referenceLinkKey>
+ <tr style="border-bottom: 1px solid #C7D1D8; padding: 16px;">
+ <td style="padding: 6px 12px;">
+ <a href="${referenceLinks[referenceLinkKey]}"
style="text-decoration: none; color:#0073B1; font-size:12px;
font-weight:bold;">${referenceLinkKey}</a>
+ </td>
+ </tr>
+ </#list>
+ </table>
+ </@utils.addBlock>
+ </#if>
+
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td style="text-align: center; background-color: #EDF0F3; font-size: 12px;
font-family:'Proxima Nova','Arial', 'Helvetica Neue',Helvetica, sans-serif;
color: #737373; padding: 12px;">
+ <p style="margin-top:0;"> You are receiving this email because you have
subscribed to ThirdEye Alert Service for
+ <strong>${alertConfigName}</strong>.</p>
+ <p>
+ If you have any questions regarding this report, please email
+ <a style="color: #33aada;" href="mailto:[email protected]"
target="_top">[email protected]</a>
+ </p>
+ </td>
+ </tr>
+
+</table>
+</body>
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
new file mode 100644
index 0000000..47c2405
--- /dev/null
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/ContentFormatterUtils.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2014-2018 LinkedIn Corp. ([email protected])
+ *
+ * Licensed 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.pinot.thirdeye.alert.content;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import org.apache.commons.mail.HtmlEmail;
+import org.apache.pinot.thirdeye.alert.commons.EmailEntity;
+
+
+public class ContentFormatterUtils {
+
+ static String getHtmlContent(String htmlPath) throws IOException {
+ StringBuilder htmlContent;
+ try (BufferedReader br = new BufferedReader(new FileReader(htmlPath))) {
+ htmlContent = new StringBuilder();
+ for (String line = br.readLine(); line != null; line = br.readLine()) {
+ htmlContent.append(line).append("\n");
+ }
+ }
+
+ return htmlContent.toString();
+ }
+
+ static String getEmailHtml(EmailEntity emailEntity) throws Exception {
+ HtmlEmail email = emailEntity.getContent();
+ Field field = email.getClass().getDeclaredField("html");
+ field.setAccessible(true);
+
+ return field.get(email).toString();
+ }
+}
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
similarity index 59%
copy from
thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
copy to
thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
index bcc3808..8ce134d 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestEntityGroupByEmailContentFormatter.java
@@ -16,6 +16,15 @@
package org.apache.pinot.thirdeye.alert.content;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
import org.apache.pinot.thirdeye.alert.commons.EmailEntity;
import org.apache.pinot.thirdeye.anomaly.ThirdEyeAnomalyConfiguration;
import org.apache.pinot.thirdeye.anomaly.monitor.MonitorConfiguration;
@@ -24,26 +33,15 @@ import org.apache.pinot.thirdeye.anomaly.utils.EmailUtils;
import org.apache.pinot.thirdeye.anomalydetection.context.AnomalyResult;
import org.apache.pinot.thirdeye.common.time.TimeGranularity;
import org.apache.pinot.thirdeye.datalayer.DaoTestUtils;
-import org.apache.pinot.thirdeye.datalayer.bao.AnomalyFunctionManager;
import org.apache.pinot.thirdeye.datalayer.bao.DAOTestBase;
+import org.apache.pinot.thirdeye.datalayer.bao.DetectionConfigManager;
import org.apache.pinot.thirdeye.datalayer.bao.MergedAnomalyResultManager;
import org.apache.pinot.thirdeye.datalayer.bao.MetricConfigManager;
-import org.apache.pinot.thirdeye.datalayer.dto.AlertConfigDTO;
-import org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
+import org.apache.pinot.thirdeye.datalayer.dto.DetectionConfigDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
import org.apache.pinot.thirdeye.datasource.DAORegistry;
import
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.testng.Assert;
@@ -51,15 +49,16 @@ import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
+import static
org.apache.pinot.thirdeye.alert.content.EntityGroupByContentFormatter.*;
import static org.apache.pinot.thirdeye.anomaly.SmtpConfiguration.*;
-public class TestMetricAnomaliesEmailContentFormatter {
+public class TestEntityGroupByEmailContentFormatter {
private static final String TEST = "test";
private int id = 0;
private String dashboardHost = "http://localhost:8080/dashboard";
private DAOTestBase testDAOProvider;
- private AnomalyFunctionManager anomalyFunctionDAO;
+ private DetectionConfigManager detectionDAO;
private MergedAnomalyResultManager mergedAnomalyResultDAO;
private MetricConfigManager metricDAO;
@@ -67,7 +66,7 @@ public class TestMetricAnomaliesEmailContentFormatter {
public void beforeClass(){
testDAOProvider = DAOTestBase.getInstance();
DAORegistry daoRegistry = DAORegistry.getInstance();
- anomalyFunctionDAO = daoRegistry.getAnomalyFunctionDAO();
+ detectionDAO = daoRegistry.getDetectionConfigManager();
mergedAnomalyResultDAO = daoRegistry.getMergedAnomalyResultDAO();
metricDAO = daoRegistry.getMetricConfigDAO();
}
@@ -77,9 +76,18 @@ public class TestMetricAnomaliesEmailContentFormatter {
testDAOProvider.cleanup();
}
+ /**
+ * Entity Alert Trigger Condition: Sub-Entity-A OR Sub-Entity-B
+ * Where Sub-Entity-A and Sub-Entity-B anomalies have a groupKey and a
groupScore
+ */
@Test
public void testGetEmailEntity() throws Exception {
- DateTimeZone dateTimeZone = DateTimeZone.forID("America/Los_Angeles");
+ MetricConfigDTO metric = new MetricConfigDTO();
+ metric.setName(TEST);
+ metric.setDataset(TEST);
+ metric.setAlias(TEST + "::" + TEST);
+ metricDAO.save(metric);
+
ThirdEyeAnomalyConfiguration thirdeyeAnomalyConfig = new
ThirdEyeAnomalyConfiguration();
thirdeyeAnomalyConfig.setId(id);
thirdeyeAnomalyConfig.setDashboardHost(dashboardHost);
@@ -100,57 +108,61 @@ public class TestMetricAnomaliesEmailContentFormatter {
alerters.put("smtpConfiguration", smtpProps);
thirdeyeAnomalyConfig.setAlerterConfiguration(alerters);
+ DetectionConfigDTO detection = new DetectionConfigDTO();
+ detection.setName("test_report");
+ long id = detectionDAO.save(detection);
- List<AnomalyResult> anomalies = new ArrayList<>();
- AnomalyFunctionDTO anomalyFunction =
DaoTestUtils.getTestFunctionSpec(TEST, TEST);
- anomalyFunctionDAO.save(anomalyFunction);
- MergedAnomalyResultDTO anomaly = DaoTestUtils.getTestMergedAnomalyResult(
- new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
- new DateTime(2017, 11, 6, 13, 0, dateTimeZone).getMillis(),
- TEST, TEST, 0.1, 1l, new DateTime(2017, 11, 6, 10, 0,
dateTimeZone).getMillis());
- anomaly.setFunction(anomalyFunction);
- anomaly.setAvgCurrentVal(1.1);
- anomaly.setAvgBaselineVal(1.0);
- mergedAnomalyResultDAO.save(anomaly);
- anomalies.add(anomaly);
- anomaly = DaoTestUtils.getTestMergedAnomalyResult(
+ DateTimeZone dateTimeZone = DateTimeZone.forID("America/Los_Angeles");
+
+ MergedAnomalyResultDTO subGroupedAnomaly1 =
DaoTestUtils.getTestGroupedAnomalyResult(
new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
- TEST, TEST, 0.1, 1l, new DateTime(2017, 11, 6, 10, 0,
dateTimeZone).getMillis());
- anomaly.setFunction(anomalyFunction);
- anomaly.setAvgCurrentVal(0.9);
- anomaly.setAvgBaselineVal(1.0);
- mergedAnomalyResultDAO.save(anomaly);
- anomalies.add(anomaly);
+ new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+ id);
+ Map<String, String> properties = new HashMap<>();
+ properties.put(PROP_GROUP_KEY, "group-1");
+ properties.put(PROP_ENTITY_NAME, "sub-entity-A");
+ properties.put(PROP_ANOMALY_SCORE, "10");
+ subGroupedAnomaly1.setProperties(properties);
+ subGroupedAnomaly1.setChildIds(Collections.singleton(1l));
+ mergedAnomalyResultDAO.save(subGroupedAnomaly1);
- MetricConfigDTO metric = new MetricConfigDTO();
- metric.setName(TEST);
- metric.setDataset(TEST);
- metric.setAlias(TEST + "::" + TEST);
- metricDAO.save(metric);
+ MergedAnomalyResultDTO subGroupedAnomaly2 =
DaoTestUtils.getTestGroupedAnomalyResult(
+ new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
+ new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
+ new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+ id);
+ Map<String, String> properties2 = new HashMap<>();
+ properties2.put(PROP_GROUP_KEY, "group-2");
+ properties2.put(PROP_ENTITY_NAME, "sub-entity-B");
+ properties2.put(PROP_ANOMALY_SCORE, "20");
+ subGroupedAnomaly2.setProperties(properties2);
+ subGroupedAnomaly2.setChildIds(Collections.singleton(2l));
+ mergedAnomalyResultDAO.save(subGroupedAnomaly2);
- AlertConfigDTO alertConfigDTO =
DaoTestUtils.getTestAlertConfiguration("Test Config");
+ MergedAnomalyResultDTO parentGroupedAnomaly =
DaoTestUtils.getTestGroupedAnomalyResult(
+ new DateTime(2017, 11, 7, 10, 0, dateTimeZone).getMillis(),
+ new DateTime(2017, 11, 7, 17, 0, dateTimeZone).getMillis(),
+ new DateTime(2017, 11, 6, 10, 0, dateTimeZone).getMillis(),
+ id);
+ Set<MergedAnomalyResultDTO> childrenAnomalies = new HashSet<>();
+ childrenAnomalies.add(subGroupedAnomaly1);
+ childrenAnomalies.add(subGroupedAnomaly2);
+ parentGroupedAnomaly.setChildren(childrenAnomalies);
+ mergedAnomalyResultDAO.save(parentGroupedAnomaly);
+ List<AnomalyResult> anomalies = new ArrayList<>();
+ anomalies.add(parentGroupedAnomaly);
- EmailContentFormatter contentFormatter = new
MetricAnomaliesEmailContentFormatter();
+ EmailContentFormatter contentFormatter = new
EntityGroupByContentFormatter();
contentFormatter.init(new Properties(),
EmailContentFormatterConfiguration.fromThirdEyeAnomalyConfiguration(thirdeyeAnomalyConfig));
- DetectionAlertFilterRecipients recipients = new
DetectionAlertFilterRecipients(
- EmailUtils.getValidEmailAddresses("[email protected]"));
- EmailEntity emailEntity = contentFormatter.getEmailEntity(alertConfigDTO,
recipients, TEST,
- null, "", anomalies, null);
-
- String htmlPath =
ClassLoader.getSystemResource("test-metric-anomalies-template.html").getPath();
- BufferedReader br = new BufferedReader(new FileReader(htmlPath));
- StringBuilder htmlContent = new StringBuilder();
- for(String line = br.readLine(); line != null; line = br.readLine()) {
- htmlContent.append(line + "\n");
- }
- br.close();
-
- HtmlEmail email = emailEntity.getContent();
- Field field = email.getClass().getDeclaredField("html");
- field.setAccessible(true);
- String emailHtml = field.get(email).toString();
- Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
- htmlContent.toString().replaceAll("\\s", ""));
+ EmailEntity emailEntity = contentFormatter.getEmailEntity(
+ DaoTestUtils.getTestAlertConfiguration("Test Config"),
+ new
DetectionAlertFilterRecipients(EmailUtils.getValidEmailAddresses("[email protected]")),
+ TEST, null, "", anomalies, null);
+ String htmlPath =
ClassLoader.getSystemResource("test-entity-groupby-email-content-formatter.html").getPath();
+
+ Assert.assertEquals(
+ ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+ ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
}
}
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
index 72ccb1b..db3d07a 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestHierarchicalAnomaliesEmailContentFormatter.java
@@ -32,16 +32,12 @@ import
org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.datasource.DAORegistry;
import
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.testng.Assert;
@@ -158,18 +154,8 @@ public class
TestHierarchicalAnomaliesEmailContentFormatter {
null, "", anomalies, null);
String htmlPath =
ClassLoader.getSystemResource("test-hierarchical-metric-anomalies-template.html").getPath();
- BufferedReader br = new BufferedReader(new FileReader(htmlPath));
- StringBuilder htmlContent = new StringBuilder();
- for(String line = br.readLine(); line != null; line = br.readLine()) {
- htmlContent.append(line + "\n");
- }
- br.close();
-
- HtmlEmail email = emailEntity.getContent();
- Field field = email.getClass().getDeclaredField("html");
- field.setAccessible(true);
- String emailHtml = field.get(email).toString();
- Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
- htmlContent.toString().replaceAll("\\s", ""));
+ Assert.assertEquals(
+ ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+ ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
}
}
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
index bcc3808..952c5ff 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestMetricAnomaliesEmailContentFormatter.java
@@ -34,16 +34,12 @@ import
org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MetricConfigDTO;
import org.apache.pinot.thirdeye.datasource.DAORegistry;
import
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.testng.Assert;
@@ -139,18 +135,8 @@ public class TestMetricAnomaliesEmailContentFormatter {
null, "", anomalies, null);
String htmlPath =
ClassLoader.getSystemResource("test-metric-anomalies-template.html").getPath();
- BufferedReader br = new BufferedReader(new FileReader(htmlPath));
- StringBuilder htmlContent = new StringBuilder();
- for(String line = br.readLine(); line != null; line = br.readLine()) {
- htmlContent.append(line + "\n");
- }
- br.close();
-
- HtmlEmail email = emailEntity.getContent();
- Field field = email.getClass().getDeclaredField("html");
- field.setAccessible(true);
- String emailHtml = field.get(email).toString();
- Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
- htmlContent.toString().replaceAll("\\s", ""));
+ Assert.assertEquals(
+ ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+ ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
}
}
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
index 5e3c587..325d268 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/alert/content/TestOnboardingNotificationContentFormatter.java
@@ -33,16 +33,12 @@ import
org.apache.pinot.thirdeye.datalayer.dto.AnomalyFunctionDTO;
import org.apache.pinot.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import org.apache.pinot.thirdeye.datasource.DAORegistry;
import
org.apache.pinot.thirdeye.detection.alert.DetectionAlertFilterRecipients;
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
-import org.apache.commons.mail.HtmlEmail;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.testng.Assert;
@@ -133,19 +129,9 @@ public class TestOnboardingNotificationContentFormatter {
null, "", anomalies, context);
String htmlPath =
ClassLoader.getSystemResource("test-onboard-notification-email-content-formatter.html").getPath();
- BufferedReader br = new BufferedReader(new FileReader(htmlPath));
- StringBuilder htmlContent = new StringBuilder();
- for(String line = br.readLine(); line != null; line = br.readLine()) {
- htmlContent.append(line + "\n");
- }
- br.close();
-
- HtmlEmail email = emailEntity.getContent();
- Field field = email.getClass().getDeclaredField("html");
- field.setAccessible(true);
- String emailHtml = field.get(email).toString();
- Assert.assertEquals(emailHtml.replaceAll("\\s", ""),
- htmlContent.toString().replaceAll("\\s", ""));
+ Assert.assertEquals(
+ ContentFormatterUtils.getEmailHtml(emailEntity).replaceAll("\\s", ""),
+ ContentFormatterUtils.getHtmlContent(htmlPath).replaceAll("\\s", ""));
}
}
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
index 3f11d17..cf92740 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/datalayer/DaoTestUtils.java
@@ -326,11 +326,24 @@ public class DaoTestUtils {
anomaly.setMetric(metric);
anomaly.setWeight(weight);
anomaly.setFunctionId(functionId);
+ anomaly.setDetectionConfigId(functionId);
anomaly.setCreatedTime(createdTime);
return anomaly;
}
+ public static MergedAnomalyResultDTO getTestGroupedAnomalyResult(long
startTime, long endTime, long createdTime, long id) {
+ MergedAnomalyResultDTO anomaly = new MergedAnomalyResultDTO();
+ anomaly.setStartTime(startTime);
+ anomaly.setEndTime(endTime);
+ anomaly.setCollection(null);
+ anomaly.setMetric(null);
+ anomaly.setCreatedTime(createdTime);
+ anomaly.setDetectionConfigId(id);
+
+ return anomaly;
+ }
+
public static RootcauseSessionDTO getTestRootcauseSessionResult(long start,
long end, long created, long updated,
String name, String owner, String text, String granularity, String
compareMode, Long previousId, Long anomalyId) {
RootcauseSessionDTO session = new RootcauseSessionDTO();
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
index 4af7323..e61eccd 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/detection/yaml/YamlResourceTest.java
@@ -240,7 +240,7 @@ public class YamlResourceTest {
Assert.assertEquals(alertDTO.getName(), "Subscription Group Name");
Assert.assertEquals(alertDTO.getApplication(), "test_application");
Assert.assertNotNull(alertDTO.getAlertSchemes().get("emailScheme"));
-
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("template"),
"ENTITY_REPORT");
+
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("template"),
"ENTITY_GROUPBY_REPORT");
Assert.assertEquals(alertDTO.getAlertSchemes().get("emailScheme").get("subject"),
"METRICS");
// Verify if the vector clock is updated with the updated detection
diff --git
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
index 5ac47ca..de90e00 100644
---
a/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
+++
b/thirdeye/thirdeye-pinot/src/test/java/org/apache/pinot/thirdeye/tools/RunAdhocDatabaseQueriesTool.java
@@ -17,6 +17,7 @@
package org.apache.pinot.thirdeye.tools;
import org.apache.pinot.thirdeye.anomaly.task.TaskConstants;
+import org.apache.pinot.thirdeye.constant.AnomalyResultSource;
import org.apache.pinot.thirdeye.datalayer.bao.AlertConfigManager;
import org.apache.pinot.thirdeye.datalayer.bao.AnomalyFunctionManager;
import org.apache.pinot.thirdeye.datalayer.bao.ApplicationManager;
@@ -443,10 +444,10 @@ public class RunAdhocDatabaseQueriesTool {
disableAllActiveFunction(null);
}
- private void disableAllActiveFunction(Collection<Long> exception){
+ private void disableAllActiveFunction(Collection<Long> excludeIds){
List<AnomalyFunctionDTO> functionSpecs =
anomalyFunctionDAO.findAllActiveFunctions();
for (AnomalyFunctionDTO functionSpec : functionSpecs) {
- if (functionSpec.getIsActive() && (CollectionUtils.isEmpty(exception) ||
!exception
+ if (functionSpec.getIsActive() && (CollectionUtils.isEmpty(excludeIds)
|| !excludeIds
.contains(functionSpec.getId()))) {
functionSpec.setActive(false);
anomalyFunctionDAO.update(functionSpec);
@@ -455,6 +456,20 @@ public class RunAdhocDatabaseQueriesTool {
}
/**
+ * Disable all subscription groups except groups with id in excludeIds
+ */
+ private void disableAllActiveSubsGroups(Collection<Long> excludeIds){
+ List<DetectionAlertConfigDTO> subsConfigs =
detectionAlertConfigDAO.findAll();
+ for (DetectionAlertConfigDTO subsConfig : subsConfigs) {
+ if (subsConfig.isActive() && (CollectionUtils.isEmpty(excludeIds) ||
!excludeIds
+ .contains(subsConfig.getId()))) {
+ subsConfig.setActive(false);
+ detectionAlertConfigDAO.update(subsConfig);
+ }
+ }
+ }
+
+ /**
* Generates a report of the status and owner of all the un-subscribed
anomaly functions
*/
private void unsubscribedDetections(){
@@ -576,6 +591,20 @@ public class RunAdhocDatabaseQueriesTool {
}
}
+ /**
+ * Replayed anomalies are flagged accordingly and such anomalies are
excluded from the email report.
+ * This method removes the replay flag to test an email report from replayed
results.
+ */
+ private void removeReplayFlagFromAnomalies(long detectionConfigId) {
+ List<MergedAnomalyResultDTO> anomalies =
mergedResultDAO.findByDetectionConfigAndIdGreaterThan(detectionConfigId,0l);
+ for (MergedAnomalyResultDTO anomaly : anomalies) {
+ if (!anomaly.isChild()) {
+
anomaly.setAnomalyResultSource(AnomalyResultSource.DEFAULT_ANOMALY_DETECTION);
+ mergedResultDAO.save(anomaly);
+ }
+ }
+ }
+
public static void main(String[] args) throws Exception {
File persistenceFile = new File(args[0]);
diff --git
a/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
b/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
index 9e7517d..9f04929 100644
---
a/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
+++
b/thirdeye/thirdeye-pinot/src/test/resources/org/apache/pinot/thirdeye/detection/yaml/alertconfig/alert-config-5.yaml
@@ -24,7 +24,7 @@ recipients:
alertSchemes:
- type: EMAIL
params:
- template: ENTITY_REPORT
+ template: ENTITY_GROUPBY_REPORT
subject: METRICS
- type: IRIS
params:
diff --git
a/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
b/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
new file mode 100644
index 0000000..0d3616f
--- /dev/null
+++
b/thirdeye/thirdeye-pinot/src/test/resources/test-entity-groupby-email-content-formatter.html
@@ -0,0 +1,105 @@
+<head>
+ <link href="https://fonts.googleapis.com/css?family=Open+Sans"
rel="stylesheet">
+</head>
+
+<body style="background-color: #EDF0F3;">
+<table border="0" cellpadding="0" cellspacing="0" style="width:100%;
font-family: 'Proxima Nova','Arial','Helvetica Neue',Helvetica,sans-serif;
font-size:14px; margin:0 auto; max-width: 50%; min-width:700px;
background-color: #F3F6F8;">
+ <!-- White bar on top -->
+ <tr>
+ <td align="center" style="padding: 6px 24px;">
+ <span style="color: red; font-size: 35px; vertical-align:
middle;"> ⚠ </span>
+ <span style="color: rgba(0,0,0,0.75);font-size: 18px; font-weight: bold;
letter-spacing: 2px; vertical-align: middle;">ACTION REQUIRED ON THIRDEYE
ALERT</span>
+ </td>
+ </tr>
+
+ <!-- Blue header on top -->
+ <tr>
+ <td style="font-size: 16px; padding: 12px; background-color: #0073B1;
color: #FFF; text-align: center;">
+ <span style="font-size: 18px; font-weight: bold; letter-spacing: 2px;
vertical-align: middle;">Report: test_report</span>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
+ <table border="0" cellpadding="0" cellspacing="0" style="border:1px
solid #E9E9E9; border-radius: 2px; width: 100%;">
+ <!-- List all the alerts -->
+ <tr>
+ <td style="border-bottom: 1px solid rgba(0,0,0,0.15); padding: 12px
24px; align: left">
+ <!-- Display Entity Name -->
+ <p>
+ <span style="color: #1D1D1D; font-size: 20px; font-weight: bold;
display:inline-block; vertical-align: middle;">Entity: </span>
+ <span style="color: #606060; font-size: 20px; text-decoration:
none; display:inline-block; vertical-align: middle; width: 70%; white-space:
nowrap; overflow: hidden; text-overflow: ellipsis;">sub-entity-B</span>
+ </p>
+ <!-- List all the anomalies under this entity in a table -->
+ <table border="0" width="100%" align="center" style="width:100%;
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+ <tr style="text-align:center; background-color: #F6F8FA;
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+ <th style="text-align:left; padding: 6px 12px; font-size:
12px; font-weight: bold; line-height: 20px;">Feature</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Criticality Score*</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Current</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Predicted</th>
+ </tr>
+ <tr style="border-bottom: 1px solid #C7D1D8;">
+ <td style="padding: 6px 12px;white-space: nowrap;">
+ <a style="font-weight: bold; text-decoration: none;
font-size:14px; line-height:20px; color: #0073B1;"
href="http://localhost:8080/dashboard/app/#/anomalies?anomalyIds=2"
+ target="_blank">group-2</a>
+ <span style="color: rgba(0,0,0,0.6); font-size:12px;
line-height:16px;">7 hours</span>
+ </td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">20</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">0</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">
+ 0
+ <div style="font-size: 12px; color:#3A8C18;">(+100.00%)</div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style="border-bottom: 1px solid rgba(0,0,0,0.15); padding: 12px
24px; align: left">
+ <!-- Display Entity Name -->
+ <p>
+ <span style="color: #1D1D1D; font-size: 20px; font-weight: bold;
display:inline-block; vertical-align: middle;">Entity: </span>
+ <span style="color: #606060; font-size: 20px; text-decoration:
none; display:inline-block; vertical-align: middle; width: 70%; white-space:
nowrap; overflow: hidden; text-overflow: ellipsis;">sub-entity-A</span>
+ </p>
+ <!-- List all the anomalies under this entity in a table -->
+ <table border="0" width="100%" align="center" style="width:100%;
padding:0; margin:0; border-collapse: collapse;text-align:left;">
+ <tr style="text-align:center; background-color: #F6F8FA;
border-top: 2px solid #C7D1D8; border-bottom: 2px solid #C7D1D8;">
+ <th style="text-align:left; padding: 6px 12px; font-size:
12px; font-weight: bold; line-height: 20px;">Feature</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Criticality Score*</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Current</th>
+ <th style="padding: 6px 12px; font-size: 12px; font-weight:
bold; line-height: 20px;">Predicted</th>
+ </tr>
+ <tr style="border-bottom: 1px solid #C7D1D8;">
+ <td style="padding: 6px 12px;white-space: nowrap;">
+ <a style="font-weight: bold; text-decoration: none;
font-size:14px; line-height:20px; color: #0073B1;"
href="http://localhost:8080/dashboard/app/#/anomalies?anomalyIds=1"
+ target="_blank">group-1</a>
+ <span style="color: rgba(0,0,0,0.6); font-size:12px;
line-height:16px;">7 hours</span>
+ </td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">10</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">0</td>
+ <td style="color: rgba(0,0,0,0.9); font-size:14px;
line-height:20px; text-align:center;">
+ 0
+ <div style="font-size: 12px; color:#3A8C18;">(+100.00%)</div>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <!-- Reference Links -->
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td style="text-align: center; background-color: #EDF0F3; font-size: 12px;
font-family:'Proxima Nova','Arial', 'Helvetica Neue',Helvetica, sans-serif;
color: #737373; padding: 12px;">
+ <p style="margin-top:0;"> You are receiving this email because you have
subscribed to ThirdEye Alert Service for
+ <strong>Test Config</strong>.</p>
+ <p>
+ If you have any questions regarding this report, please email
+ <a style="color: #33aada;" href="mailto:[email protected]"
target="_top">[email protected]</a>
+ </p>
+ </td>
+ </tr>
+</table>
+</body>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]