package com.linkedin.thirdeye.anomaly.alert.grouping; import com.fasterxml.jackson.databind.ObjectMapper; import com.linkedin.thirdeye.api.DimensionMap; import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO; import com.linkedin.thirdeye.datalayer.dto.GroupedAnomalyResultsDTO; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; import org.apache.commons.collections.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A grouper that groups anomalies by the specified dimensions. If "RollUp" option is enabled, then the groups that * contains only one anomaly will be grouped together with an empty group key. Each group of anomalies could have * a list of auxiliary email recipients, which will be append to the global email recipients. * * An usage example: * Assume that we have four anomalies, whose dimensions are enclosed in brackets: a1={D1=G1, D2=V1}, a2={D1=G1, D2=V2}, * a3={D1=G2, D2=V3}, and a4={D1=G3, D2=V4}. We further assume that we want to group by dimension D1 with roll up * enabled, then this grouper returns this grouped result: * groupKey={D1=G1} : a1, a2 * groupKey={} : a3, a4 * * User could assign the auxiliary email recipients as follows and retrieved through group keys: * {{D1=G1}:"group1AuxiliaryRecipents.com",{}:"rollUp.com"} */ public class DimensionalAlertGrouper extends BaseAlertGrouper { private static final Logger LOG = LoggerFactory.getLogger(DimensionalAlertGrouper.class); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); // Used when the user does not specify any dimensions to group by private static final DummyAlertGrouper DUMMY_ALERT_GROUPER = new DummyAlertGrouper(); public static final String GROUP_BY_KEY = "dimensions"; public static final String AUXILIARY_RECIPIENTS_MAP_KEY = "auxiliaryRecipients"; public static final String ROLL_UP_SINGLE_DIMENSION_KEY = "rollUp"; public static final String GROUP_BY_SEPARATOR = ","; // The dimension names to group the anomalies (e.g., country, page_name) private List<String> groupByDimensions = new ArrayList<>(); // The map from a dimension map to auxiliary email recipients private NavigableMap<DimensionMap, String> auxiliaryEmailRecipients = new TreeMap<>(); // Rollup the groups of anomaly, which contains only one anomaly, to a group private boolean doRollUp = false; // For testing purpose NavigableMap<DimensionMap, String> getAuxiliaryEmailRecipients() { return auxiliaryEmailRecipients; } @Override public void setParameters(Map<String, String> props) { super.setParameters(props); // Initialize dimension names to be grouped by if (this.props.containsKey(GROUP_BY_KEY)) { String[] dimensions = this.props.get(GROUP_BY_KEY).split(GROUP_BY_SEPARATOR); for (String dimension : dimensions) { groupByDimensions.add(dimension.trim()); } } // Initialize the lookup table for overriding thresholds if (props.containsKey(AUXILIARY_RECIPIENTS_MAP_KEY)) { String recipientsJsonPayLoad = props.get(AUXILIARY_RECIPIENTS_MAP_KEY); try { Map<String, String> rawAuxiliaryRecipientsMap = OBJECT_MAPPER.readValue(recipientsJsonPayLoad, HashMap.class); for (Map.Entry<String, String> auxiliaryRecipientsEntry : rawAuxiliaryRecipientsMap.entrySet()) { DimensionMap dimensionMap = new DimensionMap(auxiliaryRecipientsEntry.getKey()); String recipients = auxiliaryRecipientsEntry.getValue(); auxiliaryEmailRecipients.put(dimensionMap, recipients); } } catch (IOException e) { LOG.error("Failed to reconstruct auxiliary recipients mappings from this json string: {}", recipientsJsonPayLoad); } } // Initialize roll up parameter if (props.containsKey(ROLL_UP_SINGLE_DIMENSION_KEY)) { String doRollUpString = props.get(ROLL_UP_SINGLE_DIMENSION_KEY); try { doRollUp = Boolean.parseBoolean(doRollUpString); } catch (Exception e) { LOG.error("Failed to read RollUp parameter; RollUp is set to {}", doRollUp); } } } @Override public Map<DimensionMap, GroupedAnomalyResultsDTO> group(List<MergedAnomalyResultDTO> anomalyResults) { if (CollectionUtils.isEmpty(groupByDimensions)) { return DUMMY_ALERT_GROUPER.group(anomalyResults); } else { Map<DimensionMap, GroupedAnomalyResultsDTO> groupedAnomaliesMap = new HashMap<>(); for (MergedAnomalyResultDTO anomalyResult : anomalyResults) { DimensionMap anomalyDimensionMap = anomalyResult.getDimensions(); DimensionMap alertGroupKey = this.constructGroupKey(anomalyDimensionMap); if (groupedAnomaliesMap.containsKey(alertGroupKey)) { GroupedAnomalyResultsDTO groupedAnomalyResults = groupedAnomaliesMap.get(alertGroupKey); groupedAnomalyResults.getAnomalyResults().add(anomalyResult); } else { GroupedAnomalyResultsDTO groupedAnomalyResults = new GroupedAnomalyResultsDTO(); groupedAnomalyResults.getAnomalyResults().add(anomalyResult); groupedAnomaliesMap.put(alertGroupKey, groupedAnomalyResults); } } // Group all grouped anomalies that have only one anomaly if (doRollUp) { GroupedAnomalyResultsDTO rolledUpGroupedAnomaly = new GroupedAnomalyResultsDTO(); List<MergedAnomalyResultDTO> groupedAnomalyList = rolledUpGroupedAnomaly.getAnomalyResults(); Iterator<Map.Entry<DimensionMap, GroupedAnomalyResultsDTO>> iterator = groupedAnomaliesMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<DimensionMap, GroupedAnomalyResultsDTO> entry = iterator.next(); List<MergedAnomalyResultDTO> groupedAnomalyResults = entry.getValue().getAnomalyResults(); if (CollectionUtils.isNotEmpty(groupedAnomalyResults) && groupedAnomalyResults.size() == 1) { groupedAnomalyList.add(groupedAnomalyResults.get(0)); iterator.remove(); } } if (groupedAnomalyList.size() > 0) { groupedAnomaliesMap.put(new DimensionMap(), rolledUpGroupedAnomaly); } } return groupedAnomaliesMap; } } @Override public String groupEmailRecipients(DimensionMap alertGroupKey) { if (alertGroupKey == null || !auxiliaryEmailRecipients.containsKey(alertGroupKey)) { return EMPTY_RECIPIENTS; } else { return auxiliaryEmailRecipients.get(alertGroupKey); } } private DimensionMap constructGroupKey(DimensionMap rawKey) { DimensionMap alertGroupKey = new DimensionMap(); for (String groupByDimensionName : groupByDimensions) { if (rawKey.containsKey(groupByDimensionName)) { alertGroupKey.put(groupByDimensionName, rawKey.get(groupByDimensionName)); } } return alertGroupKey; } }