374 lines
17 KiB
Java
374 lines
17 KiB
Java
/*
|
|
* Copyright (c) 2014 Hewlett-Packard 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.app;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
import javax.annotation.Nullable;
|
|
import javax.inject.Inject;
|
|
|
|
import kafka.javaapi.producer.Producer;
|
|
import kafka.producer.KeyedMessage;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import com.google.common.collect.BiMap;
|
|
import com.google.common.collect.HashBiMap;
|
|
import com.google.common.collect.Sets;
|
|
|
|
import monasca.api.MonApiConfiguration;
|
|
import monasca.api.app.command.UpdateAlarmDefinitionCommand;
|
|
import monasca.common.model.event.AlarmDefinitionCreatedEvent;
|
|
import monasca.common.model.event.AlarmDefinitionDeletedEvent;
|
|
import monasca.common.model.event.AlarmDefinitionUpdatedEvent;
|
|
import monasca.common.model.event.AlarmDeletedEvent;
|
|
import monasca.common.model.alarm.AlarmExpression;
|
|
import monasca.common.model.alarm.AlarmSubExpression;
|
|
import monasca.common.model.metric.MetricDefinition;
|
|
import monasca.api.domain.exception.EntityExistsException;
|
|
import monasca.api.domain.exception.EntityNotFoundException;
|
|
import monasca.api.domain.exception.InvalidEntityException;
|
|
import monasca.api.domain.model.alarm.Alarm;
|
|
import monasca.api.domain.model.alarm.AlarmRepository;
|
|
import monasca.api.domain.model.alarmdefinition.AlarmDefinition;
|
|
import monasca.api.domain.model.alarmdefinition.AlarmDefinitionRepository;
|
|
import monasca.api.domain.model.notificationmethod.NotificationMethodRepository;
|
|
import monasca.common.util.Exceptions;
|
|
import monasca.common.util.Serialization;
|
|
|
|
/**
|
|
* Services alarm definition related requests.
|
|
*/
|
|
public class AlarmDefinitionService {
|
|
private static final Logger LOG = LoggerFactory.getLogger(AlarmService.class);
|
|
|
|
private final MonApiConfiguration config;
|
|
private final Producer<String, String> producer;
|
|
private final AlarmDefinitionRepository repo;
|
|
private final AlarmRepository alarmRepo;
|
|
private final NotificationMethodRepository notificationMethodRepo;
|
|
long eventCount;
|
|
|
|
@Inject
|
|
public AlarmDefinitionService(MonApiConfiguration config, Producer<String, String> producer,
|
|
AlarmDefinitionRepository repo, AlarmRepository alarmRepo,
|
|
NotificationMethodRepository notificationMethodRepo) {
|
|
this.config = config;
|
|
this.producer = producer;
|
|
this.repo = repo;
|
|
this.alarmRepo = alarmRepo;
|
|
this.notificationMethodRepo = notificationMethodRepo;
|
|
}
|
|
|
|
static class SubExpressions {
|
|
/** Sub expressions which have been removed from an updated alarm expression. */
|
|
Map<String, AlarmSubExpression> oldAlarmSubExpressions;
|
|
/** Sub expressions which have had their operator or threshold changed. */
|
|
Map<String, AlarmSubExpression> changedSubExpressions;
|
|
/** Sub expressions which have not changed. */
|
|
Map<String, AlarmSubExpression> unchangedSubExpressions;
|
|
/** Sub expressions which have been added to an updated alarm expression. */
|
|
Map<String, AlarmSubExpression> newAlarmSubExpressions;
|
|
}
|
|
|
|
/**
|
|
* Creates an alarm definition and publishes an AlarmDefinitionCreatedEvent. Note, the event is
|
|
* published first since chances of failure are higher.
|
|
*
|
|
* @throws EntityExistsException if an alarm already exists for the name
|
|
* @throws InvalidEntityException if one of the actions cannot be found
|
|
*/
|
|
public AlarmDefinition create(String tenantId, String name, @Nullable String description,
|
|
String severity, String expression, AlarmExpression alarmExpression, List<String> matchBy,
|
|
List<String> alarmActions, @Nullable List<String> okActions,
|
|
@Nullable List<String> undeterminedActions) {
|
|
// Assert no alarm exists by the name
|
|
if (repo.exists(tenantId, name))
|
|
throw new EntityExistsException(
|
|
"An alarm definition already exists for project / tenant: %s named: %s", tenantId, name);
|
|
|
|
assertActionsExist(tenantId, alarmActions, okActions, undeterminedActions);
|
|
|
|
Map<String, AlarmSubExpression> subAlarms = new HashMap<String, AlarmSubExpression>();
|
|
for (AlarmSubExpression subExpression : alarmExpression.getSubExpressions())
|
|
subAlarms.put(UUID.randomUUID().toString(), subExpression);
|
|
|
|
String alarmDefId = UUID.randomUUID().toString();
|
|
AlarmDefinition alarm = null;
|
|
|
|
try {
|
|
LOG.debug("Creating alarm definition {} for tenant {}", name, tenantId);
|
|
alarm =
|
|
repo.create(tenantId, alarmDefId, name, description, severity, expression, subAlarms,
|
|
matchBy, alarmActions, okActions, undeterminedActions);
|
|
|
|
// Notify interested parties of new alarm
|
|
String event =
|
|
Serialization.toJson(new AlarmDefinitionCreatedEvent(tenantId, alarmDefId, name,
|
|
description, expression, subAlarms, matchBy));
|
|
producer.send(new KeyedMessage<>(config.eventsTopic, String.valueOf(eventCount++), event));
|
|
|
|
return alarm;
|
|
} catch (Exception e) {
|
|
if (alarm != null)
|
|
try {
|
|
repo.deleteById(tenantId, alarm.getId());
|
|
} catch (Exception ignore) {
|
|
}
|
|
throw Exceptions.uncheck(e, "Error creating alarm definition for project / tenant %s",
|
|
tenantId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the alarm definition identified by the {@code alarmDefId}.
|
|
*
|
|
* @throws EntityNotFoundException if the alarm cannot be found
|
|
*/
|
|
public void delete(String tenantId, String alarmDefId) {
|
|
Map<String, MetricDefinition> subAlarmMetricDefs =
|
|
repo.findSubAlarmMetricDefinitions(alarmDefId);
|
|
|
|
// Have to get information about the Alarms before they are deleted. They will be deleted
|
|
// by the database as a cascade delete from the Alarm Definition delete
|
|
final List<Alarm> alarms = alarmRepo.find(tenantId, alarmDefId, null, null, null, null);
|
|
final Map<String, Map<String, AlarmSubExpression>> alarmSubExpressions =
|
|
alarmRepo.findAlarmSubExpressionsForAlarmDefinition(alarmDefId);
|
|
|
|
repo.deleteById(tenantId, alarmDefId);
|
|
|
|
// Notify interested parties of alarm definition deletion
|
|
String event =
|
|
Serialization.toJson(new AlarmDefinitionDeletedEvent(alarmDefId, subAlarmMetricDefs));
|
|
producer.send(new KeyedMessage<>(config.eventsTopic, String.valueOf(eventCount++), event));
|
|
|
|
// Notify about the Deletion of the Alarms second because that is the order that thresh
|
|
// wants it so Alarms don't get recreated
|
|
for (final Alarm alarm : alarms) {
|
|
String alarmDeletedEvent =
|
|
Serialization.toJson(new AlarmDeletedEvent(tenantId, alarm.getId(), alarm.getMetrics(),
|
|
alarmDefId, alarmSubExpressions.get(alarm.getId())));
|
|
producer.send(new KeyedMessage<>(config.eventsTopic, String.valueOf(eventCount++), alarmDeletedEvent));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the alarm definition for the {@code tenantId} and {@code alarmDefId} to the state of
|
|
* the {@code command}.
|
|
*
|
|
* @throws EntityNotFoundException if the alarm cannot be found
|
|
* @throws InvalidEntityException if one of the actions cannot be found
|
|
*/
|
|
public AlarmDefinition update(String tenantId, String alarmDefId,
|
|
AlarmExpression alarmExpression, UpdateAlarmDefinitionCommand command) {
|
|
final AlarmDefinition oldAlarmDefinition = assertAlarmDefinitionExists(tenantId, alarmDefId, command.alarmActions, command.okActions,
|
|
command.undeterminedActions);
|
|
final SubExpressions subExpressions =
|
|
subExpressionsFor(repo.findSubExpressions(alarmDefId), alarmExpression);
|
|
validateChangesAllowed(command.matchBy, oldAlarmDefinition, subExpressions);
|
|
updateInternal(tenantId, alarmDefId, false, command.name, command.description,
|
|
command.expression, command.matchBy, command.severity, alarmExpression,
|
|
command.actionsEnabled, command.alarmActions, command.okActions,
|
|
command.undeterminedActions, subExpressions);
|
|
return new AlarmDefinition(alarmDefId, command.name, command.description, command.severity,
|
|
command.expression, command.matchBy, command.actionsEnabled, command.alarmActions,
|
|
command.okActions, command.undeterminedActions);
|
|
}
|
|
|
|
/**
|
|
* Don't allow changes that would cause existing Alarms for this AlarmDefinition to be invalidated.
|
|
*
|
|
* matchBy can't change and the expression can't change the metrics used or number of subexpressions
|
|
*/
|
|
private void validateChangesAllowed(final List<String> newMatchBy,
|
|
final AlarmDefinition oldAlarmDefinition, final SubExpressions subExpressions) {
|
|
final boolean matchBySame;
|
|
if (oldAlarmDefinition.getMatchBy() == null || oldAlarmDefinition.getMatchBy().isEmpty()) {
|
|
matchBySame = newMatchBy == null || newMatchBy.isEmpty();
|
|
}
|
|
else {
|
|
matchBySame = oldAlarmDefinition.getMatchBy().equals(newMatchBy);
|
|
}
|
|
if (!matchBySame) {
|
|
throw monasca.api.resource.exception.Exceptions.unprocessableEntity("match_by must not change");
|
|
}
|
|
if (!subExpressions.oldAlarmSubExpressions.isEmpty() || !subExpressions.newAlarmSubExpressions.isEmpty()) {
|
|
final int newCount = subExpressions.newAlarmSubExpressions.size() +
|
|
subExpressions.changedSubExpressions.size() +
|
|
subExpressions.unchangedSubExpressions.size();
|
|
if (newCount != AlarmExpression.of(oldAlarmDefinition.getExpression()).getSubExpressions().size()) {
|
|
throw monasca.api.resource.exception.Exceptions.unprocessableEntity("number of subexpressions must not change");
|
|
}
|
|
else {
|
|
throw monasca.api.resource.exception.Exceptions.unprocessableEntity("metrics in subexpression must not change");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Patches the alarm definition for the {@code tenantId} and {@code alarmDefId} to the state of
|
|
* the {@code fields}.
|
|
*
|
|
* @throws EntityNotFoundException if the alarm cannot be found
|
|
* @throws InvalidEntityException if one of the actions cannot be found
|
|
*/
|
|
public AlarmDefinition patch(String tenantId, String alarmDefId, String name, String description,
|
|
String severity, String expression, AlarmExpression alarmExpression, List<String> matchBy,
|
|
Boolean enabled, List<String> alarmActions, List<String> okActions,
|
|
List<String> undeterminedActions) {
|
|
AlarmDefinition oldAlarmDefinition =
|
|
assertAlarmDefinitionExists(tenantId, alarmDefId, alarmActions, okActions,
|
|
undeterminedActions);
|
|
name = name == null ? oldAlarmDefinition.getName() : name;
|
|
description = description == null ? oldAlarmDefinition.getDescription() : description;
|
|
expression = expression == null ? oldAlarmDefinition.getExpression() : expression;
|
|
severity = severity == null ? oldAlarmDefinition.getSeverity() : severity;
|
|
alarmExpression = alarmExpression == null ? AlarmExpression.of(expression) : alarmExpression;
|
|
enabled = enabled == null ? oldAlarmDefinition.isActionsEnabled() : enabled;
|
|
matchBy = matchBy == null ? oldAlarmDefinition.getMatchBy() : matchBy;
|
|
|
|
final SubExpressions subExpressions =
|
|
subExpressionsFor(repo.findSubExpressions(alarmDefId), alarmExpression);
|
|
validateChangesAllowed(matchBy, oldAlarmDefinition, subExpressions);
|
|
updateInternal(tenantId, alarmDefId, true, name, description, expression, matchBy, severity,
|
|
alarmExpression, enabled, alarmActions, okActions, undeterminedActions, subExpressions);
|
|
|
|
return new AlarmDefinition(alarmDefId, name, description, severity, expression, matchBy,
|
|
enabled, alarmActions == null ? oldAlarmDefinition.getAlarmActions() : alarmActions,
|
|
okActions == null ? oldAlarmDefinition.getOkActions() : okActions,
|
|
undeterminedActions == null ? oldAlarmDefinition.getUndeterminedActions() : undeterminedActions);
|
|
}
|
|
|
|
private void updateInternal(String tenantId, String alarmDefId, boolean patch, String name,
|
|
String description, String expression, List<String> matchBy, String severity,
|
|
AlarmExpression alarmExpression, Boolean enabled, List<String> alarmActions,
|
|
List<String> okActions, List<String> undeterminedActions, SubExpressions subExpressions) {
|
|
|
|
try {
|
|
LOG.debug("Updating alarm definition {} for tenant {}", name, tenantId);
|
|
repo.update(tenantId, alarmDefId, patch, name, description, expression, matchBy, severity,
|
|
enabled, subExpressions.oldAlarmSubExpressions.keySet(),
|
|
subExpressions.changedSubExpressions, subExpressions.newAlarmSubExpressions,
|
|
alarmActions, okActions, undeterminedActions);
|
|
|
|
// Notify interested parties of updated alarm
|
|
String event =
|
|
Serialization.toJson(new AlarmDefinitionUpdatedEvent(tenantId, alarmDefId, name,
|
|
description, expression, matchBy, enabled, severity, subExpressions.oldAlarmSubExpressions,
|
|
subExpressions.changedSubExpressions, subExpressions.unchangedSubExpressions,
|
|
subExpressions.newAlarmSubExpressions));
|
|
producer.send(new KeyedMessage<>(config.eventsTopic, String.valueOf(eventCount++), event));
|
|
} catch (Exception e) {
|
|
throw Exceptions.uncheck(e, "Error updating alarm definition for project / tenant %s",
|
|
tenantId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an entry containing Maps of old, changed, and new sub expressions by comparing the
|
|
* {@code alarmExpression} to the existing sub expressions for the {@code alarmDefId}.
|
|
*/
|
|
SubExpressions subExpressionsFor(final Map<String, AlarmSubExpression> initialSubExpressions,
|
|
AlarmExpression alarmExpression) {
|
|
BiMap<String, AlarmSubExpression> oldExpressions =
|
|
HashBiMap.create(initialSubExpressions);
|
|
Set<AlarmSubExpression> oldSet = oldExpressions.inverse().keySet();
|
|
Set<AlarmSubExpression> newSet = new HashSet<>(alarmExpression.getSubExpressions());
|
|
|
|
// Identify old or changed expressions
|
|
Set<AlarmSubExpression> oldOrChangedExpressions =
|
|
new HashSet<>(Sets.difference(oldSet, newSet));
|
|
|
|
// Identify new or changed expressions
|
|
Set<AlarmSubExpression> newOrChangedExpressions =
|
|
new HashSet<>(Sets.difference(newSet, oldSet));
|
|
|
|
// Find changed expressions
|
|
Map<String, AlarmSubExpression> changedExpressions = new HashMap<>();
|
|
for (Iterator<AlarmSubExpression> oldIt = oldOrChangedExpressions.iterator(); oldIt.hasNext();) {
|
|
AlarmSubExpression oldExpr = oldIt.next();
|
|
for (Iterator<AlarmSubExpression> newIt = newOrChangedExpressions.iterator(); newIt.hasNext();) {
|
|
AlarmSubExpression newExpr = newIt.next();
|
|
if (sameKeyFields(oldExpr, newExpr)) {
|
|
oldIt.remove();
|
|
newIt.remove();
|
|
changedExpressions.put(oldExpressions.inverse().get(oldExpr), newExpr);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the list of unchanged expressions
|
|
BiMap<String, AlarmSubExpression> unchangedExpressions = HashBiMap.create(oldExpressions);
|
|
unchangedExpressions.values().removeAll(oldOrChangedExpressions);
|
|
unchangedExpressions.keySet().removeAll(changedExpressions.keySet());
|
|
|
|
// Remove old sub expressions
|
|
oldExpressions.values().retainAll(oldOrChangedExpressions);
|
|
|
|
// Create IDs for new expressions
|
|
Map<String, AlarmSubExpression> newExpressions = new HashMap<>();
|
|
for (AlarmSubExpression expression : newOrChangedExpressions)
|
|
newExpressions.put(UUID.randomUUID().toString(), expression);
|
|
|
|
SubExpressions subExpressions = new SubExpressions();
|
|
subExpressions.oldAlarmSubExpressions = oldExpressions;
|
|
subExpressions.changedSubExpressions = changedExpressions;
|
|
subExpressions.unchangedSubExpressions = unchangedExpressions;
|
|
subExpressions.newAlarmSubExpressions = newExpressions;
|
|
return subExpressions;
|
|
}
|
|
|
|
/**
|
|
* Returns whether all of the metrics of {@code a} and {@code b} are the same. The Threshold
|
|
* Engine can handle any other type of change to the expression
|
|
*/
|
|
private boolean sameKeyFields(AlarmSubExpression a, AlarmSubExpression b) {
|
|
return a.getMetricDefinition().equals(b.getMetricDefinition());
|
|
}
|
|
|
|
/**
|
|
* Asserts an alarm definition exists for the {@code alarmDefId} as well as the actions.
|
|
*
|
|
* @throws EntityNotFoundException if the alarm cannot be found
|
|
*/
|
|
private AlarmDefinition assertAlarmDefinitionExists(String tenantId, String alarmDefId,
|
|
List<String> alarmActions, List<String> okActions, List<String> undeterminedActions) {
|
|
AlarmDefinition alarm = repo.findById(tenantId, alarmDefId);
|
|
assertActionsExist(tenantId, alarmActions, okActions, undeterminedActions);
|
|
return alarm;
|
|
}
|
|
|
|
private void assertActionsExist(String tenantId, List<String> alarmActions,
|
|
List<String> okActions, List<String> undeterminedActions) {
|
|
Set<String> actions = new HashSet<>();
|
|
if (alarmActions != null)
|
|
actions.addAll(alarmActions);
|
|
if (okActions != null)
|
|
actions.addAll(okActions);
|
|
if (undeterminedActions != null)
|
|
actions.addAll(undeterminedActions);
|
|
if (!actions.isEmpty())
|
|
for (String action : actions)
|
|
if (!notificationMethodRepo.exists(tenantId, action))
|
|
throw new InvalidEntityException("No notification method exists for action %s", action);
|
|
}
|
|
}
|