package com.linkedin.thirdeye.tools.anomaly.report;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.google.common.base.MoreObjects;
import com.linkedin.thirdeye.anomaly.SmtpConfiguration;
import com.linkedin.thirdeye.anomaly.ThirdEyeAnomalyConfiguration;
import com.linkedin.thirdeye.anomaly.alert.AlertTaskRunner;
import com.linkedin.thirdeye.anomaly.alert.util.EmailHelper;
import com.linkedin.thirdeye.anomalydetection.context.AnomalyFeedback;
import com.linkedin.thirdeye.constant.AnomalyFeedbackType;
import com.linkedin.thirdeye.datalayer.bao.EmailConfigurationManager;
import com.linkedin.thirdeye.datalayer.bao.MergedAnomalyResultManager;
import com.linkedin.thirdeye.datalayer.bao.MetricConfigManager;
import com.linkedin.thirdeye.datalayer.bao.jdbc.EmailConfigurationManagerImpl;
import com.linkedin.thirdeye.datalayer.bao.jdbc.MergedAnomalyResultManagerImpl;
import com.linkedin.thirdeye.datalayer.bao.jdbc.MetricConfigManagerImpl;
import com.linkedin.thirdeye.datalayer.dto.EmailConfigurationDTO;
import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO;
import com.linkedin.thirdeye.datalayer.dto.MetricConfigDTO;
import com.linkedin.thirdeye.datalayer.util.DaoProviderUtil;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import io.dropwizard.configuration.ConfigurationFactory;
import io.dropwizard.jackson.Jackson;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Validation;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.mail.HtmlEmail;
public class GenerateAnomalyReport {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory());
private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
MergedAnomalyResultManager anomalyResultManager;
MetricConfigManager metricConfigManager;
EmailConfigurationManager emailConfigurationManager;
Date startTime;
Date endTime;
List<String> collections;
String dashboardUrl;
SmtpConfiguration smtpConfiguration;
String emailRecipients;
public GenerateAnomalyReport(Date startTime, Date endTime, File persistenceConfig,
List<String> datasets, String dashboardUrl, SmtpConfiguration smtpConfiguration,
String emailRecipients) {
DaoProviderUtil.init(persistenceConfig);
anomalyResultManager = DaoProviderUtil.getInstance(MergedAnomalyResultManagerImpl.class);
metricConfigManager = DaoProviderUtil.getInstance(MetricConfigManagerImpl.class);
emailConfigurationManager = DaoProviderUtil.getInstance(EmailConfigurationManagerImpl.class);
this.startTime = startTime;
this.endTime = endTime;
this.collections = datasets;
this.dashboardUrl = dashboardUrl;
this.smtpConfiguration = smtpConfiguration;
this.emailRecipients = emailRecipients;
System.out.println(
"building report : " + startTime + " -- " + endTime + ", for collections " + datasets);
}
void listMetric() {
for (String collection : collections) {
List<MetricConfigDTO> metrics = metricConfigManager.findActiveByDataset(collection);
for (MetricConfigDTO metric : metrics) {
System.out.println(collection + "," + metric.getName());
}
}
}
void updateEmailConfig() {
String recipients = "xyz@linkedin.com";
for (String collection : collections) {
List<EmailConfigurationDTO> emailConfigs =
emailConfigurationManager.findByCollection(collection);
for (EmailConfigurationDTO emailConfigurationDTO : emailConfigs) {
if (emailConfigurationDTO.getFunctions().size() > 0) {
emailConfigurationDTO.setToAddresses(recipients);
emailConfigurationManager.update(emailConfigurationDTO);
System.out.println("Email updated " + emailConfigurationDTO.getMetric());
}
}
}
}
void buildReport() {
List<MergedAnomalyResultDTO> anomalies = new ArrayList<>();
for (String collection : collections) {
anomalies.addAll(anomalyResultManager
.findByCollectionTime(collection, startTime.getTime(), endTime.getTime(), false));
}
if (anomalies.size() == 0) {
System.out.println("No anomalies found, please check the report config... exiting");
}
else {
Set<String> metrics = new HashSet<>();
int alertedAnomalies = 0;
int feedbackCollected = 0;
int trueAlert = 0;
int falseAlert = 0;
int nonActionable = 0;
List<AnomalyReportDTO> anomalyReportDTOList = new ArrayList<>();
for (MergedAnomalyResultDTO anomaly : anomalies) {
metrics.add(anomaly.getMetric());
AnomalyFeedback feedback = anomaly.getFeedback();
if (feedback != null) {
feedbackCollected++;
if (feedback.getFeedbackType().equals(AnomalyFeedbackType.ANOMALY)) {
trueAlert++;
} else if (feedback.getFeedbackType().equals(AnomalyFeedbackType.NOT_ANOMALY)) {
falseAlert++;
} else {
nonActionable++;
}
}
String feedbackVal = getFeedback(
feedback == null ? "NA" : feedback.getFeedbackType().name());
AnomalyReportDTO anomalyReportDTO =
new AnomalyReportDTO(String.valueOf(anomaly.getId()), feedbackVal,
String.format("%+.2f", anomaly.getWeight()), anomaly.getMetric(), new Date(anomaly.getStartTime()).toString(),
String.format("%.2f", getTimeDiffInHours(anomaly.getStartTime(), anomaly.getEndTime())),
getAnomalyURL(anomaly));
// anomalyReportDTOList.add(anomalyReportDTO);
// include notified alerts only in the email
if (anomaly.isNotified()) {
alertedAnomalies++;
anomalyReportDTOList.add(anomalyReportDTO);
}
}
Map<String, Object> templateData = new HashMap<>();
templateData.put("startTime", startTime.toString());
templateData.put("endTime", endTime.toString());
templateData.put("anomalyCount", anomalies.size());
templateData.put("metricsCount", metrics.size());
templateData.put("notifiedCount", alertedAnomalies);
templateData.put("feedbackCount", feedbackCollected);
templateData.put("trueAlertCount", trueAlert);
templateData.put("falseAlertCount", falseAlert);
templateData.put("nonActionableCount", nonActionable);
templateData.put("datasets", StringUtils.join(collections, ", "));
templateData.put("anomalyDetails", anomalyReportDTOList);
buildEmailTemplateAndSendAlert(templateData);
}
}
void buildEmailTemplateAndSendAlert(Map<String, Object> paramMap) {
HtmlEmail email = new HtmlEmail();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (Writer out = new OutputStreamWriter(baos, AlertTaskRunner.CHARSET)) {
Configuration freemarkerConfig = new Configuration(Configuration.VERSION_2_3_21);
freemarkerConfig.setClassForTemplateLoading(getClass(), "/com/linkedin/thirdeye/detector");
freemarkerConfig.setDefaultEncoding(AlertTaskRunner.CHARSET);
freemarkerConfig.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
Template template = freemarkerConfig.getTemplate("custom-anomaly-report.ftl");
template.process(paramMap, out);
String alertEmailSubject =
"Thirdeye : Daily anomaly report";
String alertEmailHtml = new String(baos.toByteArray(), AlertTaskRunner.CHARSET);
EmailHelper.sendEmailWithHtml(email, smtpConfiguration, alertEmailSubject, alertEmailHtml,
"thirdeye-dev@linkedin.com", emailRecipients);
} catch (Exception e) {
e.printStackTrace();
}
}
double getTimeDiffInHours(long start, long end) {
return Double.valueOf((end - start) / 1000) / 3600;
}
String getFeedback(String feedbackType) {
switch (feedbackType) {
case "ANOMALY":
return "Confirmed Anomaly";
case "NOT_ANOMALY":
return "False Alarm";
case "ANOMALY_NO_ACTION":
return "Not Actionable";
}
return "NA";
}
String getAnomalyURL(MergedAnomalyResultDTO anomalyResultDTO) {
String urlPart = "#view=anomalies&dataset=%s&metrics=%s¤tStart=%s¤tEnd=%s";
return dashboardUrl + String
.format(urlPart, anomalyResultDTO.getCollection(), anomalyResultDTO.getMetric(),
anomalyResultDTO.getStartTime(), anomalyResultDTO.getEndTime());
}
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("please pass report config directory path\n");
System.exit(1);
}
File configFile = new File(args[0]);
AnomalyReportConfig config = OBJECT_MAPPER.readValue(configFile, AnomalyReportConfig.class);
File persistenceFile = new File(config.getThirdEyeConfigDirectoryPath() + "/persistence.yml");
if (!persistenceFile.exists()) {
System.err.println("Missing file:" + persistenceFile);
System.exit(1);
}
File detectorConfigFile = new File(config.getThirdEyeConfigDirectoryPath() + "/detector.yml");
if (!detectorConfigFile.exists()) {
System.err.println("Missing file:" + detectorConfigFile);
System.exit(1);
}
ConfigurationFactory<ThirdEyeAnomalyConfiguration> factory =
new ConfigurationFactory<>(ThirdEyeAnomalyConfiguration.class,
Validation.buildDefaultValidatorFactory().getValidator(), Jackson.newObjectMapper(),
"");
ThirdEyeAnomalyConfiguration detectorConfig = factory.build(detectorConfigFile);
GenerateAnomalyReport reportGenerator =
new GenerateAnomalyReport(df.parse(config.getStartTimeIso()),
df.parse(config.getEndTimeIso()), persistenceFile,
Arrays.asList(config.getDatasets().split(",")), config.getTeBaseUrl(),
detectorConfig.getSmtpConfiguration(), config.getEmailRecipients());
reportGenerator.buildReport();
// reportGenerator.listMetric();
// reportGenerator.updateEmailConfig();
return;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class AnomalyReportDTO {
String metric;
String startDateTime;
String windowSize;
String lift;
String feedback;
String anomalyId;
String anomalyURL;
public AnomalyReportDTO(String anomalyId, String feedback, String lift, String metric,
String startDateTime, String windowSize, String anomalyURL) {
this.anomalyId = anomalyId;
this.feedback = feedback;
this.lift = lift;
this.metric = metric;
this.startDateTime = startDateTime;
this.windowSize = windowSize;
this.anomalyURL = anomalyURL;
}
public String getAnomalyId() {
return anomalyId;
}
public void setAnomalyId(String anomalyId) {
this.anomalyId = anomalyId;
}
public String getFeedback() {
return feedback;
}
public void setFeedback(String feedback) {
this.feedback = feedback;
}
public String getLift() {
return lift;
}
public void setLift(String lift) {
this.lift = lift;
}
public String getMetric() {
return metric;
}
public void setMetric(String metric) {
this.metric = metric;
}
public String getStartDateTime() {
return startDateTime;
}
public void setStartDateTime(String startDateTime) {
this.startDateTime = startDateTime;
}
public String getWindowSize() {
return windowSize;
}
public void setWindowSize(String windowSize) {
this.windowSize = windowSize;
}
public String getAnomalyURL() {
return anomalyURL;
}
public void setAnomalyURL(String anomalyURL) {
this.anomalyURL = anomalyURL;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("metric", metric).add("startDateTime", startDateTime)
.add("windowSize", windowSize).add("lift", lift).add("feedback", feedback).add("anomalyId", anomalyId)
.add("anomalyURL", anomalyURL).toString();
}
}
}