/* * (C) Copyright 2014,2016 Hewlett Packard Enterprise Development Company LP * * 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 java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.ArrayList; import java.sql.SQLException; import java.sql.ResultSet; import javax.inject.Inject; import javax.inject.Named; import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.Query; import org.skife.jdbi.v2.tweak.ResultSetMapper; import org.skife.jdbi.v2.StatementContext; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import monasca.api.infrastructure.persistence.PersistUtils; import monasca.common.model.alarm.AggregateFunction; import monasca.common.model.alarm.AlarmOperator; 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.util.Conversions; import monasca.api.domain.exception.EntityNotFoundException; import monasca.api.domain.model.alarmdefinition.AlarmDefinition; import monasca.api.domain.model.alarmdefinition.AlarmDefinitionRepo; import monasca.api.infrastructure.persistence.DimensionQueries; import monasca.api.infrastructure.persistence.SubAlarmDefinitionQueries; import com.google.common.base.Splitter; import com.google.common.collect.Lists; /** * Alarm repository implementation. */ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { private static final Joiner COMMA_JOINER = Joiner.on(','); private static final String SUB_ALARM_SQL = "select sa.*, sad.dimensions from sub_alarm_definition as sa " + "left join (select sub_alarm_definition_id, group_concat(dimension_name, '=', value) as dimensions from sub_alarm_definition_dimension group by sub_alarm_definition_id ) as sad " + "on sad.sub_alarm_definition_id = sa.id where sa.alarm_definition_id = :alarmDefId"; private static final String CREATE_SUB_EXPRESSION_SQL = "insert into sub_alarm_definition " + "(id, alarm_definition_id, function, metric_name, " + "operator, threshold, period, periods, is_deterministic, " + "created_at, updated_at) " + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())"; private static final String UPDATE_SUB_ALARM_DEF_SQL = "update sub_alarm_definition set " + "operator = ?, threshold = ?, is_deterministic = ?, updated_at = NOW() where id = ?"; private final DBI db; private final PersistUtils persistUtils; @Inject public AlarmDefinitionMySqlRepoImpl(@Named("mysql") DBI db, PersistUtils persistUtils) { this.db = db; this.persistUtils = persistUtils; } @Override public AlarmDefinition create(String tenantId, String id, String name, String description, String severity, String expression, Map subExpressions, List matchBy, List alarmActions, List okActions, List undeterminedActions) { Handle h = db.open(); try { h.begin(); h.insert( "insert into alarm_definition (id, tenant_id, name, description, severity, expression, match_by, actions_enabled, created_at, updated_at, deleted_at) values (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NULL)", id, tenantId, name, description, severity, expression, matchBy == null || Iterables.isEmpty(matchBy) ? null : COMMA_JOINER.join(matchBy), true); // Persist sub-alarms createSubExpressions(h, id, subExpressions); // Persist actions persistActions(h, id, AlarmState.ALARM, alarmActions); persistActions(h, id, AlarmState.OK, okActions); persistActions(h, id, AlarmState.UNDETERMINED, undeterminedActions); h.commit(); return new AlarmDefinition(id, name, description, severity, expression, matchBy, true, alarmActions, okActions == null ? Collections.emptyList() : okActions, undeterminedActions == null ? Collections.emptyList() : undeterminedActions); } catch (RuntimeException e) { h.rollback(); throw e; } finally { h.close(); } } @Override public void deleteById(String tenantId, String alarmDefId) { try (Handle h = db.open()) { if (h .update( "update alarm_definition set deleted_at = NOW() where tenant_id = ? and id = ? and deleted_at is NULL", tenantId, alarmDefId) == 0) throw new EntityNotFoundException("No alarm definition exists for %s", alarmDefId); // Cascade soft delete to alarms h.execute("delete from alarm where alarm_definition_id = :id", alarmDefId); } } @Override public String exists(String tenantId, String name) { try (Handle h = db.open()) { Map map = h .createQuery( "select id from alarm_definition where tenant_id = :tenantId and name = :name and deleted_at is NULL") .bind("tenantId", tenantId).bind("name", name).first(); if (map != null) { if (map.values().size() != 0) { return map.get("id").toString(); } else { return null; } } else { return null; } } } @SuppressWarnings("unchecked") @Override public List find(String tenantId, String name, Map dimensions, List severities, List sortBy, String offset, int limit) { try (Handle h = db.open()) { String query = " SELECT t.id, t.tenant_id, t.name, t.description, t.expression, t.severity, t.match_by," + " t.actions_enabled, t.created_at, t.updated_at, t.deleted_at, " + " GROUP_CONCAT(aa.alarm_state) AS states, " + " GROUP_CONCAT(aa.action_id) AS notificationIds " + "FROM (SELECT distinct ad.id, ad.tenant_id, ad.name, ad.description, ad.expression," + " ad.severity, ad.match_by, ad.actions_enabled, ad.created_at, " + " ad.updated_at, ad.deleted_at " + " FROM alarm_definition AS ad " + " LEFT OUTER JOIN sub_alarm_definition AS sad ON ad.id = sad.alarm_definition_id " + " LEFT OUTER JOIN sub_alarm_definition_dimension AS dim ON sad.id = dim.sub_alarm_definition_id %1$s " + " WHERE ad.tenant_id = :tenantId AND ad.deleted_at IS NULL %2$s) AS t " + "LEFT OUTER JOIN alarm_action AS aa ON t.id = aa.alarm_definition_id " + "GROUP BY t.id %3$s %4$s %5$s"; StringBuilder sbWhere = new StringBuilder(); if (name != null) { sbWhere.append(" and ad.name = :name"); } sbWhere.append(MySQLUtils.buildSeverityAndClause(severities)); String orderByPart = ""; if (sortBy != null && !sortBy.isEmpty()) { orderByPart = " order by " + COMMA_JOINER.join(sortBy); if (!orderByPart.contains("id")) { orderByPart = orderByPart + ",id"; } } else { orderByPart = " order by id "; } String limitPart = ""; if (limit > 0) { limitPart = " limit :limit"; } String offsetPart = ""; if (offset != null) { offsetPart = " offset " + offset + ' '; } String sql = String.format(query, SubAlarmDefinitionQueries.buildJoinClauseFor(dimensions), sbWhere, orderByPart, limitPart, offsetPart); Query q = h.createQuery(sql); q.bind("tenantId", tenantId); if (name != null) { q.bind("name", name); } MySQLUtils.bindSeverityToQuery(q, severities); if (limit > 0) { q.bind("limit", limit + 1); } q.registerMapper(new AlarmDefinitionMapper()); q = q.mapTo(AlarmDefinition.class); SubAlarmDefinitionQueries.bindDimensionsToQuery(q, dimensions); List resultSet = (List) q.list(); return resultSet; } } @Override public AlarmDefinition findById(String tenantId, String alarmDefId) { try (Handle h = db.open()) { String query = "SELECT alarm_definition.id, alarm_definition.tenant_id, alarm_definition.name, alarm_definition.description, " + "alarm_definition.expression, alarm_definition.severity, alarm_definition.match_by, alarm_definition.actions_enabled, " +" alarm_definition.created_at, alarm_definition.updated_at, alarm_definition.deleted_at, " + "GROUP_CONCAT(alarm_action.action_id) AS notificationIds,group_concat(alarm_action.alarm_state) AS states " + "FROM alarm_definition LEFT OUTER JOIN alarm_action ON alarm_definition.id=alarm_action.alarm_definition_id " + " WHERE alarm_definition.tenant_id=:tenantId AND alarm_definition.id=:alarmDefId AND alarm_definition.deleted_at " + " IS NULL GROUP BY alarm_definition.id"; Query q = h.createQuery(query); q.bind("tenantId", tenantId); q.bind("alarmDefId", alarmDefId); q.registerMapper(new AlarmDefinitionMapper()); q = q.mapTo(AlarmDefinition.class); AlarmDefinition alarmDefinition = (AlarmDefinition) q.first(); if(alarmDefinition == null) { throw new EntityNotFoundException("No alarm definition exists for %s", alarmDefId); } return alarmDefinition; } } @Override public Map findSubAlarmMetricDefinitions(String alarmDefId) { try (Handle h = db.open()) { List> rows = h.createQuery(SUB_ALARM_SQL).bind("alarmDefId", alarmDefId).list(); Map subAlarmMetricDefs = new HashMap<>(); for (Map row : rows) { String id = (String) row.get("id"); String metricName = (String) row.get("metric_name"); Map dimensions = DimensionQueries.dimensionsFor((String) row.get("dimensions")); subAlarmMetricDefs.put(id, new MetricDefinition(metricName, dimensions)); } return subAlarmMetricDefs; } } @Override public Map findSubExpressions(String alarmDefId) { try (Handle h = db.open()) { List> rows = h.createQuery(SUB_ALARM_SQL).bind("alarmDefId", alarmDefId).list(); Map subExpressions = new HashMap<>(); for (Map row : rows) { String id = (String) row.get("id"); AggregateFunction function = AggregateFunction.fromJson((String) row.get("function")); String metricName = (String) row.get("metric_name"); AlarmOperator operator = AlarmOperator.fromJson((String) row.get("operator")); Double threshold = (Double) row.get("threshold"); // MySQL connector returns an Integer, Drizzle returns a Long for period and periods. // Need to convert the results appropriately based on type. Integer period = Conversions.variantToInteger(row.get("period")); Integer periods = Conversions.variantToInteger(row.get("periods")); Boolean isDeterministic = (Boolean) row.get("is_deterministic"); Map dimensions = DimensionQueries.dimensionsFor((String) row.get("dimensions")); subExpressions.put( id, new AlarmSubExpression( function, new MetricDefinition(metricName, dimensions), operator, threshold, period, periods, isDeterministic ) ); } return subExpressions; } } @Override public void update(String tenantId, String id, boolean patch, String name, String description, String expression, List matchBy, String severity, boolean actionsEnabled, Collection oldSubAlarmIds, Map changedSubAlarms, Map newSubAlarms, List alarmActions, List okActions, List undeterminedActions) { Handle h = db.open(); try { h.begin(); h.insert( "update alarm_definition set name = ?, description = ?, expression = ?, match_by = ?, severity = ?, actions_enabled = ?, updated_at = NOW() where tenant_id = ? and id = ?", name, description, expression, matchBy == null || Iterables.isEmpty(matchBy) ? null : COMMA_JOINER.join(matchBy), severity, actionsEnabled, tenantId, id); // Delete old sub-alarms if (oldSubAlarmIds != null) for (String oldSubAlarmId : oldSubAlarmIds) h.execute("delete from sub_alarm_definition where id = ?", oldSubAlarmId); // Update changed sub-alarms if (changedSubAlarms != null) for (Map.Entry entry : changedSubAlarms.entrySet()) { AlarmSubExpression sa = entry.getValue(); h.execute( UPDATE_SUB_ALARM_DEF_SQL, sa.getOperator().name(), sa.getThreshold(), sa.isDeterministic(), entry.getKey() ); } // Insert new sub-alarms createSubExpressions(h, id, newSubAlarms); // Delete old actions if (patch) { deleteActions(h, id, AlarmState.ALARM, alarmActions); deleteActions(h, id, AlarmState.OK, okActions); deleteActions(h, id, AlarmState.UNDETERMINED, undeterminedActions); } else h.execute("delete from alarm_action where alarm_definition_id = ?", id); // Insert new actions persistActions(h, id, AlarmState.ALARM, alarmActions); persistActions(h, id, AlarmState.OK, okActions); persistActions(h, id, AlarmState.UNDETERMINED, undeterminedActions); h.commit(); } catch (RuntimeException e) { h.rollback(); throw e; } finally { h.close(); } } private void deleteActions(Handle handle, String id, AlarmState alarmState, List actions) { if (actions != null) handle.execute("delete from alarm_action where alarm_definition_id = ? and alarm_state = ?", id, alarmState.name()); } private void persistActions(Handle handle, String id, AlarmState alarmState, List actions) { if (actions != null) for (String action : actions) handle.insert("insert into alarm_action values (?, ?, ?)", id, alarmState.name(), action); } private void createSubExpressions(Handle handle, String id, Map alarmSubExpressions) { if (alarmSubExpressions != null) { for (Map.Entry subEntry : alarmSubExpressions.entrySet()) { String subAlarmId = subEntry.getKey(); AlarmSubExpression subExpr = subEntry.getValue(); MetricDefinition metricDef = subExpr.getMetricDefinition(); // Persist sub-alarm handle.insert(CREATE_SUB_EXPRESSION_SQL, subAlarmId, id, subExpr.getFunction().name(), metricDef.name, subExpr.getOperator().name(), subExpr.getThreshold(), subExpr.getPeriod(), subExpr.getPeriods(), subExpr.isDeterministic()); // Persist sub-alarm dimensions if (metricDef.dimensions != null && !metricDef.dimensions.isEmpty()) for (Map.Entry dimEntry : metricDef.dimensions.entrySet()) handle.insert("insert into sub_alarm_definition_dimension values (?, ?, ?)", subAlarmId, dimEntry.getKey(), dimEntry.getValue()); } } } private static class AlarmDefinitionMapper implements ResultSetMapper { private static final Splitter COMMA_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); public AlarmDefinition map(int index, ResultSet r, StatementContext ctx) throws SQLException { String notificationIds = r.getString("notificationIds"); String states = r.getString("states"); String matchBy = r.getString("match_by"); List notifications = splitStringIntoList(notificationIds); List state = splitStringIntoList(states); List match = splitStringIntoList(matchBy); List okActionIds = new ArrayList(); List alarmActionIds = new ArrayList(); List undeterminedActionIds = new ArrayList(); int stateAndActionIndex = 0; for (String singleState : state) { if (singleState.equals(AlarmState.UNDETERMINED.name())) { undeterminedActionIds.add(notifications.get(stateAndActionIndex)); } if (singleState.equals(AlarmState.OK.name())) { okActionIds.add(notifications.get(stateAndActionIndex)); } if (singleState.equals(AlarmState.ALARM.name())) { alarmActionIds.add(notifications.get(stateAndActionIndex)); } stateAndActionIndex++; } return new AlarmDefinition(r.getString("id"), r.getString("name"), r.getString("description"), r.getString("severity"), r.getString("expression"), match, r.getBoolean("actions_enabled"), alarmActionIds, okActionIds, undeterminedActionIds); } private List splitStringIntoList(String commaDelimitedString) { if (commaDelimitedString == null) { return new ArrayList(); } Iterable split = COMMA_SPLITTER.split(commaDelimitedString); return Lists.newArrayList(split); } } }