/* * Copyright (c) 2014-2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 monasca.api.infrastructure.persistence.mysql; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import monasca.api.domain.exception.EntityNotFoundException; import monasca.api.domain.model.alarm.Alarm; import monasca.api.domain.model.alarm.AlarmCount; import monasca.api.domain.model.alarm.AlarmRepo; import monasca.api.infrastructure.persistence.DimensionQueries; import monasca.api.infrastructure.persistence.PersistUtils; import monasca.common.model.alarm.AlarmSeverity; import monasca.common.model.alarm.AlarmState; import monasca.common.model.alarm.AlarmSubExpression; import monasca.common.model.metric.MetricDefinition; import monasca.common.persistence.BeanMapper; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.Query; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Named; /** * Alarmed metric repository implementation. */ public class AlarmMySqlRepoImpl implements AlarmRepo { private static final Joiner COMMA_JOINER = Joiner.on(','); private static final Splitter SPACE_SPLITTER = Splitter.on(' '); private static final Logger logger = LoggerFactory.getLogger(AlarmMySqlRepoImpl.class); private final DBI db; private final PersistUtils persistUtils; private static final String FIND_ALARM_BY_ID_SQL = "select ad.id as alarm_definition_id, ad.severity, ad.name as alarm_definition_name, " + "a.id as alarm_id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + "a.updated_at as updated_timestamp, a.created_at as created_timestamp, " + "md.name as metric_name, mdg.dimensions as metric_dimensions from alarm as a " + "inner join alarm_definition ad on ad.id = a.alarm_definition_id " + "inner join alarm_metric as am on am.alarm_id = a.id " + "inner join metric_definition_dimensions as mdd on mdd.id = am.metric_definition_dimensions_id " + "inner join metric_definition as md on md.id = mdd.metric_definition_id " + "left outer join (select dimension_set_id, name, value, group_concat(name, '=', value) as dimensions " + "from metric_dimension group by dimension_set_id) as mdg on mdg.dimension_set_id = mdd.metric_dimension_set_id " + "where ad.tenant_id = :tenantId and ad.deleted_at is null %s order by a.id %s"; private static final String FIND_ALARMS_SQL = "select ad.id as alarm_definition_id, ad.severity, ad.name as alarm_definition_name, " + "a.id as alarm_id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + "a.updated_at as updated_timestamp, a.created_at as created_timestamp, " + "md.name as metric_name, group_concat(mdim.name, '=', mdim.value order by mdim.name) as metric_dimensions " + "from alarm as a " + "inner join %1$s as alarm_id_list on alarm_id_list.id = a.id " + "inner join alarm_definition ad on ad.id = a.alarm_definition_id " + "inner join alarm_metric as am on am.alarm_id = a.id " + "inner join metric_definition_dimensions as mdd on mdd.id = am.metric_definition_dimensions_id " + "inner join metric_definition as md on md.id = mdd.metric_definition_id " + "left outer join metric_dimension as mdim on mdim.dimension_set_id = mdd.metric_dimension_set_id " + "group by a.id, md.name, mdim.dimension_set_id " + "%2$s"; @Inject public AlarmMySqlRepoImpl(@Named("mysql") DBI db, PersistUtils persistUtils) { this.db = db; this.persistUtils = persistUtils; } private void buildJoinClauseFor(Map dimensions, StringBuilder sbJoin) { if (dimensions == null) { return; } int i = 0; for (String dimension_key : dimensions.keySet()) { final String indexStr = String.valueOf(i); sbJoin.append(" inner join metric_dimension md").append(indexStr).append(" on md") .append(indexStr) .append(".name = :dname").append(indexStr); String dim_value = dimensions.get(dimension_key); if (!Strings.isNullOrEmpty(dim_value)) { sbJoin.append(" and ("); List values = Splitter.on('|').splitToList(dim_value); for (int j = 0; j < values.size(); j++) { sbJoin.append(" md").append(indexStr) .append(".value = :dvalue").append(indexStr).append('_').append(j); if (j < values.size() - 1) { sbJoin.append(" or"); } } sbJoin.append(")"); } sbJoin.append(" and mdd.metric_dimension_set_id = md") .append(indexStr).append(".dimension_set_id"); i++; } } @Override public void deleteById(String tenantId, String id) { final String sql = "delete a from alarm a where a.id = ?"; try (Handle h = db.open()) { // This will throw an EntityNotFoundException if Alarm doesn't exist or has a different tenant id findAlarm(tenantId, id, h); h.execute(sql, id); } } @Override public List find(String tenantId, String alarmDefId, String metricName, Map metricDimensions, AlarmState state, List severities, String lifecycleState, String link, DateTime stateUpdatedStart, List sortBy, String offset, int limit, boolean enforceLimit) { StringBuilder sbWhere = new StringBuilder("(select a.id " + "from alarm as a, alarm_definition as ad " + "where ad.id = a.alarm_definition_id " + " and ad.deleted_at is null " + " and ad.tenant_id = :tenantId "); if (alarmDefId != null) { sbWhere.append(" and ad.id = :alarmDefId "); } if (metricName != null) { sbWhere.append(" and a.id in (select distinct a.id from alarm as a " + "inner join alarm_metric as am on am.alarm_id = a.id " + "inner join metric_definition_dimensions as mdd " + " on mdd.id = am.metric_definition_dimensions_id " + "inner join (select distinct id from metric_definition " + " where name = :metricName) as md " + " on md.id = mdd.metric_definition_id "); buildJoinClauseFor(metricDimensions, sbWhere); sbWhere.append(")"); } else if (metricDimensions != null) { sbWhere.append(" and a.id in (select distinct a.id from alarm as a " + "inner join alarm_metric as am on am.alarm_id = a.id " + "inner join metric_definition_dimensions as mdd " + " on mdd.id = am.metric_definition_dimensions_id "); buildJoinClauseFor(metricDimensions, sbWhere); sbWhere.append(")"); } if (state != null) { sbWhere.append(" and a.state = :state"); } sbWhere.append(MySQLUtils.buildSeverityAndClause(severities)); if (lifecycleState != null) { sbWhere.append(" and a.lifecycle_state = :lifecycleState"); } if (link != null) { sbWhere.append(" and a.link = :link"); } if (stateUpdatedStart != null) { sbWhere.append(" and a.state_updated_at >= :stateUpdatedStart"); } StringBuilder orderClause = new StringBuilder(); if (sortBy != null && !sortBy.isEmpty()) { // Convert friendly names to column names replaceFieldName(sortBy, "alarm_id", "a.id"); replaceFieldName(sortBy, "alarm_definition_id", "ad.id"); replaceFieldName(sortBy, "alarm_definition_name", "ad.name"); replaceFieldName(sortBy, "created_timestamp", "a.created_at"); replaceFieldName(sortBy, "updated_timestamp", "a.updated_at"); replaceFieldName(sortBy, "state_updated_timestamp", "a.state_updated_at"); replaceFieldName(sortBy, "state", "FIELD(state, \"OK\", \"UNDETERMINED\", \"ALARM\")"); replaceFieldName(sortBy, "severity", "FIELD(severity, \"LOW\", \"MEDIUM\", \"HIGH\", \"CRITICAL\")"); orderClause.append(" order by "); orderClause.append(COMMA_JOINER.join(sortBy)); // if alarm_id is not in the list, add it if (orderClause.indexOf("a.id") == -1) { orderClause.append(",a.id ASC"); } orderClause.append(' '); } else { orderClause.append(" order by a.id ASC "); } sbWhere.append(orderClause); if (enforceLimit && limit > 0) { sbWhere.append(" limit :limit"); } if (offset != null) { sbWhere.append(" offset "); sbWhere.append(offset); sbWhere.append(' '); } sbWhere.append(")"); String sql = String.format(FIND_ALARMS_SQL, sbWhere, orderClause); try (Handle h = db.open()) { final Query> q = h.createQuery(sql).bind("tenantId", tenantId); if (alarmDefId != null) { q.bind("alarmDefId", alarmDefId); } if (metricName != null) { q.bind("metricName", metricName); } if (state != null) { q.bind("state", state.name()); } MySQLUtils.bindSeverityToQuery(q, severities); if (lifecycleState != null) { q.bind("lifecycleState", lifecycleState); } if (link != null) { q.bind("link", link); } if (stateUpdatedStart != null) { q.bind("stateUpdatedStart", stateUpdatedStart.toString()); } if (enforceLimit && limit > 0) { q.bind("limit", limit + 1); } DimensionQueries.bindDimensionsToQuery(q, metricDimensions); final List> rows = q.list(); return createAlarms(tenantId, rows); } } private void replaceFieldName(List list, String oldString, String newString) { for (int i = 0; i < list.size(); i++) { String listElement = list.get(i); String columnName = SPACE_SPLITTER.splitToList(listElement).get(0); if (columnName.equals(oldString)) { list.set(i, listElement.replace(oldString, newString)); } } } @Override public Alarm findById(String tenantId, String alarmId) { try (Handle h = db.open()) { return findAlarm(tenantId, alarmId, h); } } private Alarm findAlarm(String tenantId, String alarmId, Handle h) { final String sql = String.format(FIND_ALARM_BY_ID_SQL, " and a.id = :id", ""); final List> rows = h.createQuery(sql).bind("id", alarmId) .bind("tenantId", tenantId) .list(); if (rows.isEmpty()) { throw new EntityNotFoundException("No alarm exists for %s", alarmId); } return createAlarms(tenantId, rows).get(0); } private List createAlarms(String tenantId, List> rows) { Alarm alarm; String previousAlarmId = null; final List alarms = new LinkedList<>(); List alarmedMetrics = null; for (final Map row : rows) { final String alarmId = (String) row.get("alarm_id"); if (!alarmId.equals(previousAlarmId)) { alarmedMetrics = new ArrayList<>(); alarm = new Alarm(alarmId, getString(row, "alarm_definition_id"), getString(row, "alarm_definition_name"), getString(row, "severity"), alarmedMetrics, AlarmState.valueOf(getString(row, "state")), getString(row, "lifecycle_state"), getString(row, "link"), new DateTime(((Timestamp)row.get("state_updated_timestamp")).getTime(), DateTimeZone.forID("UTC")), new DateTime(((Timestamp)row.get("updated_timestamp")).getTime(), DateTimeZone.forID("UTC")), new DateTime(((Timestamp)row.get("created_timestamp")).getTime(), DateTimeZone.forID("UTC"))); alarms.add(alarm); } previousAlarmId = alarmId; final Map dimensionMap = new HashMap<>(); // Not all Metrics have dimensions (at least theoretically) if (row.containsKey("metric_dimensions")) { final String dimensions = getString(row, "metric_dimensions"); if (dimensions != null && !dimensions.isEmpty()) { for (String dimension : dimensions.split(",")) { final String[] parsed_dimension = dimension.split("="); if (parsed_dimension.length == 2) { dimensionMap.put(parsed_dimension[0], parsed_dimension[1]); } else { logger.error("Failed to parse dimension. Dimension is malformed: {}", dimension); } } } } alarmedMetrics.add(new MetricDefinition(getString(row, "metric_name"), dimensionMap)); } return alarms; } private String getString(final Map row, String fieldName) { return (String) row.get(fieldName); } @Override public Alarm update(String tenantId, String id, AlarmState state, String lifecycleState, String link) { Handle h = db.open(); try { h.begin(); final Alarm originalAlarm = findAlarm(tenantId, id, h); if (!originalAlarm.getState().equals(state)) { h.insert( "update alarm set state = ?, state_updated_at = NOW() where id = ?", state.name(), id); } h.insert("update alarm set lifecycle_state = ?, link = ?, updated_at = NOW() where id = ?", lifecycleState, link, id); h.commit(); return originalAlarm; } catch (RuntimeException e) { h.rollback(); throw e; } finally { h.close(); } } public static class SubAlarm { private String id; private String expression; public SubAlarm() { } public SubAlarm(String id, String expression) { this.id = id; this.expression = expression; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getExpression() { return expression; } public void setExpression(String expression) { this.expression = expression; } } @Override public Map findAlarmSubExpressions(String alarmId) { try (Handle h = db.open()) { final List result = h .createQuery("select * from sub_alarm where alarm_id = :alarmId") .bind("alarmId", alarmId) .map(new BeanMapper<>(SubAlarm.class)).list(); final Map subAlarms = new HashMap<>(result.size()); for (SubAlarm row : result) { subAlarms.put(row.id, AlarmSubExpression.of(row.expression)); } return subAlarms; } } @Override public Map> findAlarmSubExpressionsForAlarmDefinition( String alarmDefinitionId) { try (Handle h = db.open()) { final List> rows = h .createQuery( "select sa.* from sub_alarm as sa, alarm as a where sa.alarm_id=a.id and a.alarm_definition_id = :alarmDefinitionId") .bind("alarmDefinitionId", alarmDefinitionId).list(); Map> subAlarms = new HashMap<>(); for (Map row : rows) { final String alarmId = (String) row.get("alarm_id"); Map alarmMap = subAlarms.get(alarmId); if (alarmMap == null) { alarmMap = new HashMap<>(); subAlarms.put(alarmId, alarmMap); } final String id = (String) row.get("id"); final String expression = (String) row.get("expression"); alarmMap.put(id, AlarmSubExpression.of(expression)); } return subAlarms; } } @Override public AlarmCount getAlarmsCount(String tenantId, String alarmDefId, String metricName, Map metricDimensions, AlarmState state, List severities, String lifecycleState, String link, DateTime stateUpdatedStart, List groupBy, String offset, int limit) { final String SELECT_CLAUSE = "SELECT count(*) as count%1$s " + " FROM alarm AS a " + " INNER JOIN alarm_definition as ad on ad.id = a.alarm_definition_id "; StringBuilder queryBuilder = new StringBuilder(); String groupByStr = ""; String metricSelect; if (groupBy != null) { groupByStr = COMMA_JOINER.join(groupBy); queryBuilder.append(String.format(SELECT_CLAUSE, ',' + groupByStr)); if (groupBy.contains("metric_name") || groupBy.contains("dimension_name") || groupBy .contains("dimension_value")) { metricSelect = " INNER JOIN (SELECT distinct am.alarm_id%1$s " + "FROM metric_definition AS md " + "JOIN metric_definition_dimensions AS mdd on md.id = mdd.metric_definition_id " + "JOIN metric_dimension AS mdim ON mdd.metric_dimension_set_id = mdim.dimension_set_id " + "JOIN alarm_metric AS am ON am.metric_definition_dimensions_id = mdd.id)" + "AS metrics ON a.id = metrics.alarm_id "; String subSelect = ""; if (groupBy.contains("metric_name")) { subSelect = subSelect + ",md.name AS metric_name"; } if (groupBy.contains("dimension_name")) { subSelect = subSelect + ",mdim.name AS dimension_name"; } if (groupBy.contains("dimension_value")) { subSelect = subSelect + ",mdim.value AS dimension_value"; } queryBuilder.append(String.format(metricSelect, subSelect)); } } else { queryBuilder.append(String.format(SELECT_CLAUSE, groupByStr)); } queryBuilder.append(" INNER JOIN (SELECT a.id " + "FROM alarm AS a, alarm_definition AS ad " + "WHERE ad.id = a.alarm_definition_id " + " AND ad.deleted_at IS NULL " + " AND ad.tenant_id = :tenantId "); if (alarmDefId != null) { queryBuilder.append(" AND ad.id = :alarmDefId "); } if (metricName != null) { queryBuilder.append(" AND a.id IN (SELECT distinct a.id FROM alarm AS a " + "INNER JOIN alarm_metric AS am ON am.alarm_id = a.id " + "INNER JOIN metric_definition_dimensions AS mdd " + " ON mdd.id = am.metric_definition_dimensions_id " + "INNER JOIN (SELECT distinct id FROM metric_definition " + " WHERE name = :metricName) AS md " + " ON md.id = mdd.metric_definition_id "); buildJoinClauseFor(metricDimensions, queryBuilder); queryBuilder.append(")"); } else if (metricDimensions != null) { queryBuilder.append(" AND a.id IN (SELECT distinct a.id FROM alarm AS a " + "INNER JOIN alarm_metric AS am ON am.alarm_id = a.id " + "INNER JOIN metric_definition_dimensions AS mdd " + " ON mdd.id = am.metric_definition_dimensions_id "); buildJoinClauseFor(metricDimensions, queryBuilder); queryBuilder.append(")"); } if (state != null) { queryBuilder.append(" AND a.state = :state"); } queryBuilder.append(MySQLUtils.buildSeverityAndClause(severities)); if (lifecycleState != null) { queryBuilder.append(" AND a.lifecycle_state = :lifecycleState"); } if (link != null) { queryBuilder.append(" AND a.link = :link"); } if (stateUpdatedStart != null) { queryBuilder.append(" AND a.state_updated_at >= :stateUpdatedStart"); } queryBuilder.append(") AS alarm_id_list ON alarm_id_list.id = a.id "); if (groupBy != null) { queryBuilder.append(" GROUP BY "); queryBuilder.append(groupByStr); } queryBuilder.append(" ORDER BY "); if (!Strings.isNullOrEmpty(groupByStr)) { queryBuilder.append(groupByStr); } else { queryBuilder.append(" a.id "); } queryBuilder.append(" LIMIT :limit"); if (offset != null) { queryBuilder.append(String.format(" OFFSET %1$s ", offset)); } try (Handle h = db.open()) { final Query> q = h.createQuery(queryBuilder.toString()).bind("tenantId", tenantId); if (alarmDefId != null) { q.bind("alarmDefId", alarmDefId); } if (metricName != null) { q.bind("metricName", metricName); } if (state != null) { q.bind("state", state.name()); } MySQLUtils.bindSeverityToQuery(q, severities); if (lifecycleState != null) { q.bind("lifecycleState", lifecycleState); } if (link != null) { q.bind("link", link); } if (stateUpdatedStart != null) { q.bind("stateUpdatedStart", stateUpdatedStart.toString()); } q.bind("limit", limit + 1); DimensionQueries.bindDimensionsToQuery(q, metricDimensions); final List> rows = q.list(); return createAlarmCounts(groupBy, rows); } } private AlarmCount createAlarmCounts(List groupBy, List> rows) { List> counts = new ArrayList<>(); // if no results, return 0 and fill columns with null if (rows.size() == 0) { List countsAndTags = new ArrayList<>(); countsAndTags.add(0); if (groupBy != null) { for (final String columnName : groupBy) { countsAndTags.add(null); } } counts.add(countsAndTags); return new AlarmCount(groupBy, counts); } for (final Map row : rows) { List countAndTags = new ArrayList<>(); countAndTags.add(row.get("count")); if (groupBy != null && !groupBy.isEmpty()) { for (final String columnName : groupBy) { countAndTags.add(row.get(columnName)); } } counts.add(countAndTags); } return new AlarmCount(groupBy, counts); } }