diff --git a/managed/devops/bin/cluster_health.py b/managed/devops/bin/cluster_health.py index 3d6d73e87069..4e51e698c23a 100755 --- a/managed/devops/bin/cluster_health.py +++ b/managed/devops/bin/cluster_health.py @@ -499,15 +499,25 @@ def send_email(subject, msg, sender, destination): s.quit() -def send_alert_email(customer_tag, task_info_json, destination): - if task_info_json['alertname'] == 'Backup failure': - task_type = task_info_json['task_type'] - target_type = task_info_json['target_type'] - target_name = task_info_json['target_name'] - task_info = task_info_json['task_info'] - subject = "Yugabyte Platform Alert - <{}>".format(customer_tag) - msg_content = "{} {} failed for {}.\n\nTask Info: {}"\ +def send_alert_email(customer_tag, alert_info_json, destination): + is_valid = True + subject = "Yugabyte Platform Alert - <{}>".format(customer_tag) + if 'alert_name' in alert_info_json and alert_info_json['alert_name'] == 'Backup failure': + task_type = alert_info_json['task_type'] + target_type = alert_info_json['target_type'] + target_name = alert_info_json['target_name'] + task_info = alert_info_json['task_info'] + msg_content = "{} {} failed for {}.\n\nTask Info: {}" \ .format(task_type, target_type, target_name, task_info) + elif 'alert_name' in alert_info_json: + alert_name = alert_info_json['alert_name'] + alert_state = alert_info_json['state'] + universe_name = alert_info_json['universe_name'] + msg_content = "{} for {} is {}.".format(alert_name, universe_name, alert_state) + else: + logging.error("Invalid alert_info_json") + is_valid = False + if is_valid: sender = EMAIL_FROM msg = MIMEMultipart('alternative') msg['Subject'] = subject @@ -762,13 +772,13 @@ def main(): help='Only report nodes with errors') parser.add_argument('--send_notification', action="store_true", help='Whether this is to alert to notify on or not') - parser.add_argument('--task_info', type=str, default=None, required=False, + parser.add_argument('--alert_info', type=str, default=None, required=False, help='JSON serialized payload of backups that have failed') args = parser.parse_args() - if args.send_notification and args.task_info is not None: - task_info_json = json.loads(args.task_info) - send_alert_email(args.customer_tag, task_info_json, args.destination) - print(task_info_json) + if args.send_notification and args.alert_info is not None: + alert_info_json = json.loads(args.alert_info) + send_alert_email(args.customer_tag, alert_info_json, args.destination) + print(alert_info_json) elif args.cluster_payload is not None and args.universe_name is not None: universe = UniverseDefinition(args.cluster_payload) coordinator = CheckCoordinator(args.retry_interval_secs) diff --git a/managed/src/main/java/Module.java b/managed/src/main/java/Module.java index bc6afbf79785..df84bce194cd 100644 --- a/managed/src/main/java/Module.java +++ b/managed/src/main/java/Module.java @@ -5,6 +5,7 @@ import com.yugabyte.yw.cloud.AWSInitializer; import com.yugabyte.yw.commissioner.HealthChecker; import com.yugabyte.yw.commissioner.CallHome; +import com.yugabyte.yw.commissioner.QueryAlerts; import com.yugabyte.yw.commissioner.SetUniverseKey; import com.yugabyte.yw.common.*; import com.yugabyte.yw.controllers.PlatformHttpActionAdapter; @@ -78,6 +79,8 @@ public void configure() { bind(SetUniverseKey.class).asEagerSingleton(); bind(CustomerTaskManager.class).asEagerSingleton(); bind(YamlWrapper.class).asEagerSingleton(); + bind(AlertManager.class).asEagerSingleton(); + bind(QueryAlerts.class).asEagerSingleton(); final CallbackController callbackController = new CallbackController(); callbackController.setDefaultUrl(config.getString("yb.url", "")); diff --git a/managed/src/main/java/com/yugabyte/yw/commissioner/AbstractTaskBase.java b/managed/src/main/java/com/yugabyte/yw/commissioner/AbstractTaskBase.java index 40eccc7d25fb..85d6f4f79205 100644 --- a/managed/src/main/java/com/yugabyte/yw/commissioner/AbstractTaskBase.java +++ b/managed/src/main/java/com/yugabyte/yw/commissioner/AbstractTaskBase.java @@ -15,7 +15,10 @@ import com.yugabyte.yw.common.HealthManager; import com.yugabyte.yw.common.ShellProcessHandler; import com.yugabyte.yw.forms.CustomerRegisterFormData; -import com.yugabyte.yw.models.*; +import com.yugabyte.yw.models.Customer; +import com.yugabyte.yw.models.CustomerConfig; +import com.yugabyte.yw.models.CustomerTask; +import com.yugabyte.yw.models.Universe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -175,7 +178,7 @@ public void sendNotification() { .put("task_type", task.getType().name()) .put("target_type", task.getTarget().name()) .put("target_name", task.getNotificationTargetName()) - .put("task_info", taskInfo); + .put("alert_info", taskInfo); String customerTag = String.format("[%s][%s]", customer.name, customer.code); List destinations = new ArrayList<>(); String ybEmail = appConfig.getString("yb.health.default_email", null); diff --git a/managed/src/main/java/com/yugabyte/yw/commissioner/HealthChecker.java b/managed/src/main/java/com/yugabyte/yw/commissioner/HealthChecker.java index e474ec249aff..cd35ba1991ce 100644 --- a/managed/src/main/java/com/yugabyte/yw/commissioner/HealthChecker.java +++ b/managed/src/main/java/com/yugabyte/yw/commissioner/HealthChecker.java @@ -119,10 +119,9 @@ private void initialize() { ); try { - healthMetric = Gauge.build(kUnivMetricName, "Boolean result of health checks"). - labelNames(kUnivUUIDLabel, kUnivNameLabel, kNodeLabel, kCheckLabel). - register(this.promRegistry); - + healthMetric = Gauge.build(kUnivMetricName, "Boolean result of health checks") + .labelNames(kUnivUUIDLabel, kUnivNameLabel, kNodeLabel, kCheckLabel) + .register(this.promRegistry); } catch (IllegalArgumentException e) { LOG.warn("Failed to build prometheus gauge for name: " + kUnivMetricName); } diff --git a/managed/src/main/java/com/yugabyte/yw/commissioner/QueryAlerts.java b/managed/src/main/java/com/yugabyte/yw/commissioner/QueryAlerts.java new file mode 100644 index 000000000000..0598931a3e4e --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/commissioner/QueryAlerts.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020 YugaByte, Inc. and Contributors + * + * Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +package com.yugabyte.yw.commissioner; + +import akka.actor.ActorSystem; +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.yugabyte.yw.common.AlertManager; +import com.yugabyte.yw.metrics.MetricQueryHelper; +import com.yugabyte.yw.models.*; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import scala.concurrent.ExecutionContext; +import scala.concurrent.duration.Duration; + +@Singleton +public class QueryAlerts { + public static final Logger LOG = LoggerFactory.getLogger(QueryAlerts.class); + + private AtomicBoolean running = new AtomicBoolean(false); + + private final ActorSystem actorSystem; + + private final ExecutionContext executionContext; + + private final MetricQueryHelper queryHelper; + + private final AlertManager alertManager; + + private final int YB_QUERY_ALERTS_INTERVAL = 1; + + @Inject + public QueryAlerts( + ExecutionContext executionContext, + ActorSystem actorSystem, + AlertManager alertManager, + MetricQueryHelper queryHelper + ) { + this.actorSystem = actorSystem; + this.executionContext = executionContext; + this.queryHelper = queryHelper; + this.alertManager = alertManager; + this.initialize(); + } + + public void setRunningState(AtomicBoolean state) { + this.running = state; + } + + private void initialize() { + this.actorSystem.scheduler().schedule( + Duration.create(0, TimeUnit.MINUTES), + Duration.create(YB_QUERY_ALERTS_INTERVAL, TimeUnit.MINUTES), + this::scheduleRunner, + this.executionContext + ); + } + + public Set processAlertDefinitions(UUID customerUUID) { + Set alertsStillActive = new HashSet<>(); + AlertDefinition.listActive(customerUUID).forEach(definition -> { + if (!queryHelper.queryDirect(definition.query).isEmpty()) { + Universe universe = Universe.get(definition.universeUUID); + Alert existingAlert = Alert.getActiveCustomerAlert(customerUUID, definition.uuid); + // Create an alert to activate if it doesn't exist already + if (existingAlert == null) { + Alert.create( + customerUUID, + definition.universeUUID, + Alert.TargetType.UniverseType, + "CUSTOMER_ALERT", + "Error", + String.format("%s for %s is firing", definition.name, universe.name), + definition.isActive, + definition.uuid + ); + } else { + alertsStillActive.add(existingAlert); + } + } + }); + + return alertsStillActive; + } + + @VisibleForTesting + void scheduleRunner() { + if (running.compareAndSet(false, true)) { + try { + Set alertsStillActive = new HashSet<>(); + + // Pick up all alerts still active + create new alerts + Customer.getAll().forEach(c -> alertsStillActive.addAll(processAlertDefinitions(c.uuid))); + + // Pick up all created alerts that are waiting to be activated + Set alertsToTransition = new HashSet<>(Alert.listToActivate()); + + // Pick up all alerts that should be resolved internally but are currently active + Customer.getAll().forEach(c -> + Alert.listActiveCustomerAlerts(c.uuid).forEach(alert -> { + if (!alertsStillActive.contains(alert)) alertsToTransition.add(alert); + })); + + // Trigger alert transitions + alertsToTransition.forEach(alertManager::transitionAlert); + } catch (Exception e) { + LOG.error("Error querying for alerts", e); + } + + running.set(false); + } + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/AlertManager.java b/managed/src/main/java/com/yugabyte/yw/common/AlertManager.java new file mode 100644 index 000000000000..ecc3401781c7 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/common/AlertManager.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020 YugaByte, Inc. and Contributors + * + * Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +package com.yugabyte.yw.common; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.yugabyte.yw.forms.CustomerRegisterFormData; +import com.yugabyte.yw.models.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import play.libs.Json; +import play.Configuration; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class AlertManager { + @Inject + HealthManager healthManager; + + @Inject + Configuration appConfig; + + + public static final Logger LOG = LoggerFactory.getLogger(AlertManager.class); + + public void sendEmail(Alert alert, String state) { + if (!alert.sendEmail) { + return; + } + + AlertDefinition definition = AlertDefinition.get(alert.definitionUUID); + Universe universe = Universe.get(definition.universeUUID); + ObjectNode alertData = Json.newObject() + .put("alert_name", definition.name) + .put("state", state) + .put("universe_name", universe.name); + Customer customer = Customer.get(alert.customerUUID); + String customerTag = String.format("[%s][%s]", customer.name, customer.code); + List destinations = new ArrayList<>(); + String ybEmail = appConfig.getString("yb.health.default_email", null); + CustomerConfig config = CustomerConfig.getAlertConfig(customer.uuid); + CustomerRegisterFormData.AlertingData alertingData = + Json.fromJson(config.data, CustomerRegisterFormData.AlertingData.class); + if (alertingData.sendAlertsToYb && ybEmail != null && !ybEmail.isEmpty()) { + destinations.add(ybEmail); + } + + if (alertingData.alertingEmail != null && !alertingData.alertingEmail.isEmpty()) { + destinations.add(alertingData.alertingEmail); + } + + // Skip sending email if there aren't any destinations to send it to + if (destinations.isEmpty()) { + return; + } + + CustomerConfig smtpConfig = CustomerConfig.getSmtpConfig(customer.uuid); + CustomerRegisterFormData.SmtpData smtpData = null; + if (smtpConfig != null) { + smtpData = Json.fromJson(smtpConfig.data, CustomerRegisterFormData.SmtpData.class); + } + + healthManager.runCommand( + customerTag, + String.join(",", destinations), + smtpData, + alertData + ); + } + + /** + * A method to run a state transition for a given alert + * + * @param alert the alert to transition states on + * @return the alert in a new state + */ + public Alert transitionAlert(Alert alert) { + try { + switch (alert.state) { + case CREATED: + LOG.info("Transitioning alert {} to active", alert.uuid); + sendEmail(alert, "firing"); + alert.state = Alert.State.ACTIVE; + break; + case ACTIVE: + LOG.info("Transitioning alert {} to resolved", alert.uuid); + sendEmail(alert, "resolved"); + alert.state = Alert.State.RESOLVED; + break; + case RESOLVED: + LOG.info("Transitioning alert {} to resolved", alert.uuid); + alert.state = Alert.State.RESOLVED; + break; + } + + alert.save(); + } catch (Exception e) { + LOG.error("Error transitioning alert state for alert {}", alert.uuid, e); + } + + return alert; + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/HealthManager.java b/managed/src/main/java/com/yugabyte/yw/common/HealthManager.java index 8ab3b9ee9eeb..facabb6beb69 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/HealthManager.java +++ b/managed/src/main/java/com/yugabyte/yw/common/HealthManager.java @@ -174,7 +174,7 @@ public ShellProcessHandler.ShellResponse runCommand( if (isTaskNotification) { commandArgs.add("--send_notification"); - commandArgs.add("--task_info"); + commandArgs.add("--alert_info"); commandArgs.add(Json.stringify(taskInfo)); } diff --git a/managed/src/main/java/com/yugabyte/yw/controllers/AlertController.java b/managed/src/main/java/com/yugabyte/yw/controllers/AlertController.java index d3392c6129a4..44fd15a79325 100644 --- a/managed/src/main/java/com/yugabyte/yw/controllers/AlertController.java +++ b/managed/src/main/java/com/yugabyte/yw/controllers/AlertController.java @@ -17,9 +17,8 @@ import com.google.inject.Inject; import com.yugabyte.yw.common.ApiResponse; -import com.yugabyte.yw.models.Audit; -import com.yugabyte.yw.models.Alert; -import com.yugabyte.yw.models.Customer; +import com.yugabyte.yw.forms.AlertDefinitionFormData; +import com.yugabyte.yw.models.*; import com.yugabyte.yw.forms.AlertFormData; import org.slf4j.Logger; @@ -55,6 +54,23 @@ public Result list(UUID customerUUID) { return ok(alerts); } + public Result listActive(UUID customerUUID) { + try { + if (Customer.get(customerUUID) == null) { + return ApiResponse.error(BAD_REQUEST, "Invalid Customer UUID: " + customerUUID); + } + + ArrayNode alerts = Json.newArray(); + for (Alert alert: Alert.listActive(customerUUID)) { + alerts.add(alert.toJson()); + } + + return ok(alerts); + } catch (Exception e) { + return ApiResponse.error(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + /** * Upserts alert of specified errCode with new message and createTime. Creates alert if needed. * This may only be used to create or update alerts that have 1 or fewer entries in the DB. @@ -104,4 +120,82 @@ public Result create(UUID customerUUID) { Audit.createAuditEntry(ctx(), request(), Json.toJson(formData.data())); return ok(); } + + public Result createDefinition(UUID customerUUID, UUID universeUUID) { + try { + if (Customer.get(customerUUID) == null) { + return ApiResponse.error(BAD_REQUEST, "Invalid Customer UUID: " + customerUUID); + } + + Universe universe = Universe.get(universeUUID); + + Form formData = + formFactory.form(AlertDefinitionFormData.class).bindFromRequest(); + if (formData.hasErrors()) { + return ApiResponse.error(BAD_REQUEST, formData.errorsAsJson()); + } + + AlertDefinitionFormData data = formData.get(); + + AlertDefinition definition = AlertDefinition.create( + customerUUID, + universeUUID, + data.name, + data.template.buildQuery(universe.getUniverseDetails().nodePrefix, data.value), + data.isActive + ); + + return ok(Json.toJson(definition)); + } catch (Exception e) { + LOG.error("Error creating alert definition", e); + return ApiResponse.error(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + public Result getAlertDefinition(UUID customerUUID, UUID universeUUID, String name) { + if (Customer.get(customerUUID) == null) { + return ApiResponse.error(BAD_REQUEST, "Invalid Customer UUID: " + customerUUID); + } + + AlertDefinition definition = AlertDefinition.get(customerUUID, universeUUID, name); + + if (definition == null) { + return ApiResponse.error(BAD_REQUEST, "Could not find Alert Definition"); + } + + return ok(Json.toJson(definition)); + } + + public Result updateAlertDefinition(UUID customerUUID, UUID alertDefinitionUUID) { + try { + if (Customer.get(customerUUID) == null) { + return ApiResponse.error(BAD_REQUEST, "Invalid Customer UUID: " + customerUUID); + } + + AlertDefinition definition = AlertDefinition.get(alertDefinitionUUID); + if (definition == null) { + return ApiResponse.error(BAD_REQUEST, "Invalid Alert Definition UUID: " + alertDefinitionUUID); + } + + Universe universe = Universe.get(definition.universeUUID); + + Form formData = + formFactory.form(AlertDefinitionFormData.class).bindFromRequest(); + if (formData.hasErrors()) { + return ApiResponse.error(BAD_REQUEST, formData.errorsAsJson()); + } + + AlertDefinitionFormData data = formData.get(); + definition = AlertDefinition.update( + definition.uuid, + data.template.buildQuery(universe.getUniverseDetails().nodePrefix, data.value), + data.isActive + ); + + return ok(Json.toJson(definition)); + } catch (Exception e) { + LOG.error("Error updating alert definition", e); + return ApiResponse.error(INTERNAL_SERVER_ERROR, e.getMessage()); + } + } } diff --git a/managed/src/main/java/com/yugabyte/yw/forms/AlertDefinitionFormData.java b/managed/src/main/java/com/yugabyte/yw/forms/AlertDefinitionFormData.java new file mode 100644 index 000000000000..eee09bc50bc8 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/forms/AlertDefinitionFormData.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 YugaByte, Inc. and Contributors + * + * Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +package com.yugabyte.yw.forms; + +import play.data.validation.Constraints; + +import java.util.UUID; + +/** + * This class will be used by the API and UI Form Elements to validate constraints are met. + */ +public class AlertDefinitionFormData { + public enum TemplateType { + REPLICATION_LAG("max by (node_prefix) (avg_over_time(async_replication_committed_lag_micros" + + "{node_prefix=\"__nodePrefix__\"}[10m]) or avg_over_time(async_replication_sent_lag_micros" + + "{node_prefix=\"__nodePrefix__\"}[10m])) / 1000 > __value__"); + + private String template; + + public String buildQuery(String nodePrefix, double value) { + switch (this) { + case REPLICATION_LAG: + return template + .replaceAll("__nodePrefix__", nodePrefix) + .replaceAll("__value__", Double.toString(value)); + default: + throw new RuntimeException("Invalid alert definition template provided"); + } + } + + TemplateType(String template) { + this.template = template; + } + } + + public UUID alertDefinitionUUID; + + public TemplateType template; + + @Constraints.Required() + public double value; + + @Constraints.Required() + public String name; + + @Constraints.Required() + public boolean isActive; +} diff --git a/managed/src/main/java/com/yugabyte/yw/models/Alert.java b/managed/src/main/java/com/yugabyte/yw/models/Alert.java index c2c183aa5012..f8201eab9c37 100644 --- a/managed/src/main/java/com/yugabyte/yw/models/Alert.java +++ b/managed/src/main/java/com/yugabyte/yw/models/Alert.java @@ -30,7 +30,7 @@ public enum TargetType { @EnumValue("UniverseType") UniverseType; - public Class getType() { + public Class getType() { switch (this) { case UniverseType: return Universe.class; @@ -38,15 +38,15 @@ public Class getType() { return null; } } + } - public static TargetType getType(CustomerTask.TargetType targetType) { - switch (targetType) { - case Universe: - return UniverseType; - default: - return null; - } - } + public enum State { + @EnumValue("CREATED") + CREATED, + @EnumValue("ACTIVE") + ACTIVE, + @EnumValue("RESOLVED") + RESOLVED } @Constraints.Required @@ -82,12 +82,20 @@ public static TargetType getType(CustomerTask.TargetType targetType) { @Column(columnDefinition = "Text", nullable = false) public String message; + @Enumerated(EnumType.STRING) + public State state; + + @Constraints.Required + public boolean sendEmail; + + public UUID definitionUUID; + public static final Logger LOG = LoggerFactory.getLogger(Alert.class); private static final Finder find = new Finder(Alert.class) {}; public static Alert create( - UUID customerUUID, UUID targetUUID, TargetType targetType,String errCode, - String type, String message) { + UUID customerUUID, UUID targetUUID, TargetType targetType, String errCode, + String type, String message, boolean sendEmail, UUID definitionUUID) { Alert alert = new Alert(); alert.uuid = UUID.randomUUID(); alert.customerUUID = customerUUID; @@ -97,10 +105,28 @@ public static Alert create( alert.errCode = errCode; alert.type = type; alert.message = message; + alert.sendEmail = sendEmail; + alert.state = State.CREATED; + alert.definitionUUID = definitionUUID; alert.save(); return alert; } + public static Alert create( + UUID customerUUID, UUID targetUUID, TargetType targetType, String errCode, + String type, String message) { + return Alert.create( + customerUUID, + targetUUID, + targetType, + errCode, + type, + message, + false, + null + ); + } + public static Alert create(UUID customerUUID, String errCode, String type, String message) { return Alert.create(customerUUID, null, null, errCode, type, message); } @@ -118,7 +144,8 @@ public JsonNode toJson() { .put("createTime", createTime.toString()) .put("errCode", errCode) .put("type", type) - .put("message", message); + .put("message", message) + .put("state", state.name()); return json; } @@ -131,6 +158,14 @@ public static Boolean exists(String errCode, UUID targetUUID) { .eq("target_uuid", targetUUID).findCount() != 0; } + public static Alert getActiveCustomerAlert(UUID customerUUID, UUID definitionUUID) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("state", State.ACTIVE) + .eq("definition_uuid", definitionUUID) + .findOne(); + } + public static List list(UUID customerUUID) { return find.query().where() .eq("customer_uuid", customerUUID) @@ -143,6 +178,28 @@ public static List list(UUID customerUUID, String errCode) { .eq("errCode", errCode).findList(); } + public static List listToActivate() { + return find.query().where() + .eq("state", State.CREATED) + .findList(); + } + + public static List listActive(UUID customerUUID) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("state", State.ACTIVE) + .orderBy("create_time desc") + .findList(); + } + + public static List listActiveCustomerAlerts(UUID customerUUID) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("state", State.ACTIVE) + .eq("err_code", "CUSTOMER_ALERT") + .findList(); + } + public static Alert get(UUID customerUUID, String errCode, UUID targetUUID) { return find.query().where().eq("customer_uuid", customerUUID) .eq("errCode", errCode) @@ -152,4 +209,12 @@ public static Alert get(UUID customerUUID, String errCode, UUID targetUUID) { public static Alert get(UUID alertUUID) { return find.query().where().idEq(alertUUID).findOne(); } + + public static Alert get(UUID customerUUID, UUID targetUUID, TargetType targetType) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("target_uuid", targetUUID) + .eq("target_type", targetType) + .findOne(); + } } diff --git a/managed/src/main/java/com/yugabyte/yw/models/AlertDefinition.java b/managed/src/main/java/com/yugabyte/yw/models/AlertDefinition.java new file mode 100644 index 000000000000..618849a58797 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/models/AlertDefinition.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 YugaByte, Inc. and Contributors + * + * Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/YugaByte/yugabyte-db/blob/master/licenses/POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +package com.yugabyte.yw.models; + +import io.ebean.Finder; +import io.ebean.Model; +import play.data.validation.Constraints; + +import javax.persistence.*; +import java.util.Set; +import java.util.UUID; + +@Entity +public class AlertDefinition extends Model { + @Constraints.Required + @Id + @Column(nullable = false, unique = true) + public UUID uuid; + + @Constraints.Required + @Column(columnDefinition = "Text", nullable = false) + public String name; + + @Constraints.Required + @Column(columnDefinition = "Text", nullable = false) + public UUID universeUUID; + + @Constraints.Required + @Column(columnDefinition = "Text", nullable = false) + public String query; + + @Constraints.Required + public boolean isActive; + + @Constraints.Required + @Column(nullable = false) + public UUID customerUUID; + + private static final Finder find = + new Finder(AlertDefinition.class) {}; + + public static AlertDefinition create( + UUID customerUUID, + UUID universeUUID, + String name, + String query, + boolean isActive + ) { + AlertDefinition definition = new AlertDefinition(); + definition.uuid = UUID.randomUUID(); + definition.name = name; + definition.customerUUID = customerUUID; + definition.universeUUID = universeUUID; + definition.query = query; + definition.isActive = isActive; + definition.save(); + + return definition; + } + + public static AlertDefinition get(UUID alertDefinitionUUID) { + return find.query().where().idEq(alertDefinitionUUID).findOne(); + } + + public static AlertDefinition get(UUID customerUUID, UUID universeUUID, String name) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("universe_uuid", universeUUID) + .eq("name", name) + .findOne(); + } + + public static AlertDefinition update( + UUID alertDefinitionUUID, + String query, + boolean isActive + ) { + AlertDefinition alertDefinition = get(alertDefinitionUUID); + alertDefinition.query = query; + alertDefinition.isActive = isActive; + alertDefinition.save(); + + return alertDefinition; + } + + public static Set listActive(UUID customerUUID) { + return find.query().where() + .eq("customer_uuid", customerUUID) + .eq("is_active", true) + .findSet(); + } +} diff --git a/managed/src/main/resources/db/migration/default/V55__Alter_Alert_Table.sql b/managed/src/main/resources/db/migration/default/V55__Alter_Alert_Table.sql new file mode 100644 index 000000000000..7a3e85298017 --- /dev/null +++ b/managed/src/main/resources/db/migration/default/V55__Alter_Alert_Table.sql @@ -0,0 +1,18 @@ +-- Copyright (c) YugaByte, Inc. +ALTER TABLE alert ADD COLUMN state varchar(64) default 'ACTIVE'; +ALTER TABLE alert ADD COLUMN send_email boolean default false; +ALTER TABLE alert ADD COLUMN definition_uuid uuid default null; + +create table alert_definition ( + uuid uuid not null, + query TEXT not null, + name TEXT not null, + universe_uuid uuid not null, + is_active boolean default false, + customer_uuid uuid not null, + constraint pk_alert_definition primary key (uuid), + constraint fk_customer_uuid foreign key (customer_uuid) references customer (uuid), + constraint fk_universe_uuid foreign key (universe_uuid) references universe (universe_uuid) +); + +ALTER TABLE alert add CONSTRAINT fk_alert_definition_uuid FOREIGN KEY (definition_uuid) references alert_definition (uuid); diff --git a/managed/src/main/resources/v1.routes b/managed/src/main/resources/v1.routes index 16a75647957b..d47f777a11b3 100644 --- a/managed/src/main/resources/v1.routes +++ b/managed/src/main/resources/v1.routes @@ -90,6 +90,10 @@ POST /customers/:cUUID/certificates/:rUUID/update_empty_cert c GET /customers/:cUUID/alerts com.yugabyte.yw.controllers.AlertController.list(cUUID: java.util.UUID) PUT /customers/:cUUID/alerts com.yugabyte.yw.controllers.AlertController.upsert(cUUID: java.util.UUID) POST /customers/:cUUID/alerts com.yugabyte.yw.controllers.AlertController.create(cUUID: java.util.UUID) +GET /customers/:cUUID/alerts/active com.yugabyte.yw.controllers.AlertController.listActive(cUUID: java.util.UUID) +POST /customers/:cUUID/universes/:uniUUID/alert_definitions com.yugabyte.yw.controllers.AlertController.createDefinition(cUUID: java.util.UUID, uniUUID: java.util.UUID) +GET /customers/:cUUID/alert_definitions/:uniUUID/:name com.yugabyte.yw.controllers.AlertController.getAlertDefinition(cUUID: java.util.UUID, uniUUID: java.util.UUID, name: String) +PUT /customers/:cUUID/alert_definitions/:dUUID com.yugabyte.yw.controllers.AlertController.updateAlertDefinition(cUUID: java.util.UUID, dUUID: java.util.UUID) # Access Key API GET /customers/:cUUID/providers/:pUUID/access_keys/:keyCode com.yugabyte.yw.controllers.AccessKeyController.index(cUUID: java.util.UUID, pUUID: java.util.UUID, keyCode: java.lang.String) diff --git a/managed/src/test/java/com/yugabyte/yw/common/HealthManagerTest.java b/managed/src/test/java/com/yugabyte/yw/common/HealthManagerTest.java index 2f10f78be1b6..415f78c221b6 100644 --- a/managed/src/test/java/com/yugabyte/yw/common/HealthManagerTest.java +++ b/managed/src/test/java/com/yugabyte/yw/common/HealthManagerTest.java @@ -114,7 +114,7 @@ public void testHealthManager() { System.out.println("running, reportOnlyErrors = " + reportOnlyErrors.toString()); healthManager.runCommand( provider, ImmutableList.of(cluster), universeName, customerTag, d, startTime, - sendStatus, reportOnlyErrors, null, false, Json.newArray()); + sendStatus, reportOnlyErrors, null); HashMap extraEnvVars = new HashMap<>(provider.getConfig()); if (envVal != null) { extraEnvVars.put("YB_ALERTS_USERNAME", envVal); diff --git a/managed/ui/src/actions/customers.js b/managed/ui/src/actions/customers.js index 80c2e2ada877..03b09862ea23 100644 --- a/managed/ui/src/actions/customers.js +++ b/managed/ui/src/actions/customers.js @@ -420,7 +420,7 @@ export function fetchCustomerCount() { export function getAlerts() { const cUUID = localStorage.getItem('customerId'); - const request = axios.get(`${ROOT_URL}/customers/${cUUID}/alerts`); + const request = axios.get(`${ROOT_URL}/customers/${cUUID}/alerts/active`); return { type: GET_ALERTS, payload: request diff --git a/managed/ui/src/actions/universe.js b/managed/ui/src/actions/universe.js index 80ea52c3f4b9..bc74fbeb1f22 100644 --- a/managed/ui/src/actions/universe.js +++ b/managed/ui/src/actions/universe.js @@ -113,6 +113,13 @@ export const SET_ALERTS_CONFIG_RESPONSE = 'SET_ALERTS_CONFIG_RESPONSE'; export const UPDATE_BACKUP_STATE = 'UPDATE_BACKUP_STATE'; export const UPDATE_BACKUP_STATE_RESPONSE = 'UPDATE_BACKUP_STATE_RESPONSE'; +export const CREATE_ALERT_DEFINITION = 'CREATE_ALERT_DEFINITION'; +export const CREATE_ALERT_DEFINITION_RESPONSE = 'CREATE_ALERT_DEFINITION_RESPONSE'; +export const GET_ALERT_DEFINITION = 'GET_ALERT_DEFINITION'; +export const GET_ALERT_DEFINITION_RESPONSE = 'GET_ALERT_DEFINITION_RESPONSE'; +export const UPDATE_ALERT_DEFINITION = "UPDATE_ALERT_DEFINITION"; +export const UPDATE_ALERT_DEFINITION_RESPONSE = "UPDATE_ALERT_DEFINITION_RESPONSE"; + export function createUniverse(formValues) { const customerUUID = localStorage.getItem('customerId'); const request = axios.post(`${ROOT_URL}/customers/${customerUUID}/universes`, formValues); @@ -650,4 +657,58 @@ export function fetchLiveQueries(universeUUID, cancelFn) { } return request; -} \ No newline at end of file +} + +export function createAlertDefinition(universeUUID, data) { + const customerUUID = localStorage.getItem('customerId'); + const endpoint = `${ROOT_URL}/customers/${customerUUID}/universes/${universeUUID}/alert_definitions`; + const request = axios.post(endpoint, data); + + return { + type: CREATE_ALERT_DEFINITION, + payload: request + }; +} + +export function createAlertDefinitionResponse(response) { + return { + type: CREATE_ALERT_DEFINITION_RESPONSE, + payload: response + }; +} + +export function getAlertDefinition(universeUUID, name) { + const customerUUID = localStorage.getItem('customerId'); + const endpoint = `${ROOT_URL}/customers/${customerUUID}/alert_definitions/${universeUUID}/${name}`; + const request = axios.get(endpoint); + + return { + type: GET_ALERT_DEFINITION, + payload: request + }; +} + +export function getAlertDefinitionResponse(response) { + return { + type: GET_ALERT_DEFINITION_RESPONSE, + payload: response + }; +} + +export function updateAlertDefinition(alertDefinitionUUID, data) { + const customerUUID = localStorage.getItem('customerId'); + const endpoint = `${ROOT_URL}/customers/${customerUUID}/alert_definitions/${alertDefinitionUUID}`; + const request = axios.put(endpoint, data); + + return { + type: UPDATE_ALERT_DEFINITION, + payload: request + }; +} + +export function updateAlertDefinitionResponse(response) { + return { + type: UPDATE_ALERT_DEFINITION_RESPONSE, + payload: response + }; +} diff --git a/managed/ui/src/components/alerts/AlertList/AlertsList.js b/managed/ui/src/components/alerts/AlertList/AlertsList.js index 8734f7affeff..759519d98340 100644 --- a/managed/ui/src/components/alerts/AlertList/AlertsList.js +++ b/managed/ui/src/components/alerts/AlertList/AlertsList.js @@ -34,7 +34,7 @@ export default class AlertsList extends Component { columnClassName="no-border" className="no-border" dataAlign="left" - width={'10%'} + width={'20%'} > Time @@ -51,7 +51,7 @@ export default class AlertsList extends Component { dataField="errCode" columnClassName="no-border name-column" className="no-border" - width={'10%'} + width={'20%'} > Error Code @@ -59,7 +59,7 @@ export default class AlertsList extends Component { dataField="message" columnClassName="no-border name-column" className="no-border" - width={'70%'} + width={'50%'} tdStyle={{ whiteSpace: 'normal' }} > Message diff --git a/managed/ui/src/components/tables/Replication/Replication.js b/managed/ui/src/components/tables/Replication/Replication.js index 86e842b712b0..048c5b9951ee 100644 --- a/managed/ui/src/components/tables/Replication/Replication.js +++ b/managed/ui/src/components/tables/Replication/Replication.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import _ from 'lodash'; -import { isNonEmptyArray } from '../../../utils/ObjectUtils'; +import { isDefinedNotNull, isNonEmptyArray, isNullOrEmpty } from '../../../utils/ObjectUtils'; import { getPromiseState } from '../../../utils/PromiseUtils'; import { YBPanelItem } from '../../panels'; import { YBLoading } from '../../common/indicators'; @@ -12,18 +12,33 @@ import { YBResourceCount } from '../../common/descriptors'; import { MetricsPanel } from '../../metrics'; import './Replication.scss'; +import {Row, Col, Alert} from 'react-bootstrap'; +import YBToggle from "../../common/forms/fields/YBToggle"; +import {change, Field} from "redux-form"; +import {YBButton, YBNumericInput} from "../../common/forms/fields"; const GRAPH_TYPE = 'replication'; const METRIC_NAME = 'tserver_async_replication_lag_micros'; const MILLI_IN_MIN = 60000.0; const MILLI_IN_SEC = 1000.0; +const ALERT_NAME = "Replication Lag Alert"; +const ALERT_AUTO_DISMISS_MS = 4000; +const ALERT_TEMPLATE = "REPLICATION_LAG"; -export default class ListBackups extends Component { +export default class Replication extends Component { constructor(props) { super(props); + this.isComponentMounted = false; this.state = { - graphWidth: 840 + graphWidth: 840, + showNotification: false, + alertDefinitionChanged: false, + enableAlert: props.initialValues.enableAlert, + threshold: props.initialValues.value }; + this.toggleEnableAlert = this.toggleEnableAlert.bind(this); + this.shouldShowSaveButton = this.shouldShowSaveButton.bind(this); + this.changeThreshold = this.changeThreshold.bind(this); } static defaultProps = { @@ -31,20 +46,111 @@ export default class ListBackups extends Component { }; static propTypes = { - currentUniverse: PropTypes.object.isRequired + universe: PropTypes.object.isRequired }; componentDidMount() { const { graph } = this.props; + const { alertDefinition } = this.state; this.queryMetrics(graph.graphFilter); + this.getAlertDefinition(); + if (isDefinedNotNull(alertDefinition)) { + const value = Replication.extractValueFromQuery(alertDefinition.query); + if (value !== null) this.updateFormField("value", value); + this.updateFormField("enableAlert", alertDefinition.isActive); + } + + this.isComponentMounted = true; + } + + static getDerivedStateFromProps(props, state) { + const { universe: { alertDefinition }} = props; + if (getPromiseState(alertDefinition).isSuccess() && isNullOrEmpty(state.alertDefinition)) { + state.enableAlert = alertDefinition.data.isActive; + state.alertDefinition = alertDefinition.data; + state.threshold = Replication.extractValueFromQuery(alertDefinition.data.query); + } else if (getPromiseState(alertDefinition).isLoading()) { + state.alertDefinition = null; + } + + return state; + } + + componentDidUpdate(prevProps, prevState) { + const { enableAlert, threshold } = this.state; + this.updateFormField("value", threshold); + this.updateFormField("enableAlert", enableAlert); } componentWillUnmount() { - this.props.resetMasterLeader(); + const { resetMasterLeader } = this.props; + resetMasterLeader(); + this.isComponentMounted = false; + } + + static extractValueFromQuery(query) { + const result = query.match(/[0-9]+(.)?[0-9]*$/g); + if (result.length === 1) return result[0]; + else return null; + } + + createAlertForm = (values) => { + const { universe: { currentUniverse }} = this.props; + const formData = { + name: ALERT_NAME, + isActive: values.enableAlert, + template: ALERT_TEMPLATE, + value: values.value + }; + const universeUUID = currentUniverse.data.universeUUID; + if (values.enableAlert) { + this.props.createAlertDefinition(universeUUID, formData); + this.setState({ + showNotification: true, + alertDefinition: null, + alertDefinitionChanged: false + }); + setTimeout(() => { + if (this.isComponentMounted) { + this.setState({ + showNotification: false + }); + } + }, ALERT_AUTO_DISMISS_MS); + } + }; + + editAlertForm = (values) => { + const formData = { + name: ALERT_NAME, + isActive: values.enableAlert, + template: ALERT_TEMPLATE, + value: values.value + }; + const alertDefinitionUUID = this.state.alertDefinition.uuid; + this.props.updateAlertDefinition(alertDefinitionUUID, formData); + this.setState({ + showNotification: true, + alertDefinition: null, + alertDefinitionChanged: false + }); + setTimeout(() => { + if (this.isComponentMounted) { + this.setState({ + showNotification: false + }); + } + }, ALERT_AUTO_DISMISS_MS); + }; + + getAlertDefinition = () => { + const { getAlertDefinition, universe: { currentUniverse }, alertDefinition } = this.props; + const universeUUID = currentUniverse.data.universeUUID; + if (alertDefinition === null) getAlertDefinition(universeUUID, ALERT_NAME); } queryMetrics = (graphFilter) => { - const { currentUniverse } = this.props; + const { universe: { currentUniverse }} = this.props; const universeDetails = getPromiseState(currentUniverse).isSuccess() ? currentUniverse.data.universeDetails : 'all'; @@ -57,12 +163,76 @@ export default class ListBackups extends Component { this.props.queryMetrics(params, GRAPH_TYPE); }; + updateFormField = (field, value) => { + this.props.dispatch(change('replicationLagAlertForm', field, value)); + }; + + closeAlert = () => { + this.setState({ showNotification: false }); + }; + + shouldShowSaveButton = () => { + const { alertDefinition, alertDefinitionChanged, enableAlert } = this.state; + return (isDefinedNotNull(alertDefinition) && alertDefinitionChanged) || enableAlert; + } + + toggleEnableAlert(event) { + const { alertDefinition, enableAlert, threshold } = this.state; + const currentValue = event.target.checked; + let hasChanged; + if (isDefinedNotNull(alertDefinition)) { + if (currentValue === false && alertDefinition.isActive === false) { + hasChanged = false; + } else { + const existingThreshold = Replication.extractValueFromQuery(alertDefinition.query); + hasChanged = currentValue !== alertDefinition.isActive || + parseFloat(existingThreshold) !== parseFloat(threshold); + } + } else { + hasChanged = currentValue !== enableAlert; + } + + this.setState({ + enableAlert: currentValue, + alertDefinitionChanged: hasChanged + }); + this.updateFormField("enableAlert", currentValue); + } + + changeThreshold(value) { + const { alertDefinition, enableAlert, threshold } = this.state; + let hasChanged; + if (isDefinedNotNull(alertDefinition)) { + const existingThreshold = Replication.extractValueFromQuery(alertDefinition.query); + hasChanged = enableAlert !== alertDefinition.isActive || + parseFloat(existingThreshold) !== parseFloat(value); + } else { + hasChanged = parseFloat(threshold) !== parseFloat(value); + } + + this.setState({ + alertDefinitionChanged: hasChanged, + threshold: value + }); + this.updateFormField("value", value); + } + render() { const { title, - currentUniverse, + universe: { currentUniverse }, graph: { metrics } } = this.props; + const { + alertDefinition, + alertDefinitionChanged, + enableAlert, + showNotification + } = this.state; + const alertDefinitionExists = isDefinedNotNull(alertDefinition); + const submitAction = alertDefinitionExists ? this.editAlertForm : this.createAlertForm; + const showSaveButton = alertDefinitionExists ? alertDefinitionChanged : enableAlert; + const universeDetails = currentUniverse.data.universeDetails; const nodeDetails = universeDetails.nodeDetailsSet; if (!isNonEmptyArray(nodeDetails)) { @@ -151,12 +321,70 @@ export default class ListBackups extends Component { // TODO: Make graph resizeable return (
+ {showNotification && + +

{'Success'}

+

{'Saved Alert Definition'}

+
+ } +

{title}

+ {showMetrics && +
+
+ + + + + + + {(enableAlert || showSaveButton) && + + {enableAlert && + + + + + + } + {showSaveButton && + + + + + + } +
+ } + /> + } + + +
+ } } body={ diff --git a/managed/ui/src/components/tables/Replication/ReplicationContainer.js b/managed/ui/src/components/tables/Replication/ReplicationContainer.js index 5a86b6332e36..ecf45bf2811d 100644 --- a/managed/ui/src/components/tables/Replication/ReplicationContainer.js +++ b/managed/ui/src/components/tables/Replication/ReplicationContainer.js @@ -9,10 +9,14 @@ import { resetMetrics } from '../../../actions/graph'; import { + createAlertDefinition, createAlertDefinitionResponse, + getAlertDefinition, + getAlertDefinitionResponse, getMasterLeader, getMasterLeaderResponse, - resetMasterLeader + resetMasterLeader, updateAlertDefinition, updateAlertDefinitionResponse } from '../../../actions/universe'; +import {reduxForm} from "redux-form"; const mapDispatchToProps = (dispatch) => { return { @@ -36,16 +40,45 @@ const mapDispatchToProps = (dispatch) => { resetMasterLeader: () => { dispatch(resetMasterLeader()); + }, + + createAlertDefinition: (uuid, data) => { + dispatch(createAlertDefinition(uuid, data)).then((response) => { + dispatch(createAlertDefinitionResponse(response.payload)); + }); + }, + + getAlertDefinition: (uuid, name) => { + dispatch(getAlertDefinition(uuid, name)).then((response) => { + dispatch(getAlertDefinitionResponse(response.payload)); + }); + }, + + updateAlertDefinition: (uuid, data) => { + dispatch(updateAlertDefinition(uuid, data)).then((response) => { + dispatch(updateAlertDefinitionResponse(response.payload)); + }); } }; }; -function mapStateToProps(state, ownProps) { +function mapStateToProps(state) { + const { universe } = state; return { - currentUniverse: state.universe.currentUniverse, currentCustomer: state.customer.currentCustomer, - graph: state.graph + alertDefinition: null, + graph: state.graph, + universe: universe, + initialValues: { + enableAlert: false, + value: 180000 + } }; } -export default connect(mapStateToProps, mapDispatchToProps)(Replication); +const replicationForm = reduxForm({ + form: 'replicationLagAlertForm', + fields: ['enableAlert', 'value'] +}); + +export default connect(mapStateToProps, mapDispatchToProps)(replicationForm(Replication)); diff --git a/managed/ui/src/components/tasks/TaskList/TaskListTable.js b/managed/ui/src/components/tasks/TaskList/TaskListTable.js index bbae2f7c00c6..19d864ea7b06 100644 --- a/managed/ui/src/components/tasks/TaskList/TaskListTable.js +++ b/managed/ui/src/components/tasks/TaskList/TaskListTable.js @@ -15,10 +15,11 @@ export default class TaskListTable extends Component { }; static propTypes = { taskList: PropTypes.array.isRequired, - overrideContent: PropTypes.object + overrideContent: PropTypes.object, + isCommunityEdition: PropTypes.bool }; render() { - const { taskList, title, overrideContent } = this.props; + const { taskList, title, overrideContent, isCommunityEdition } = this.props; function nameFormatter(cell, row) { return {row.title.replace(/.*:\s*/, '')}; @@ -44,7 +45,7 @@ export default class TaskListTable extends Component { {title}} body={ - !!overrideContent ? ( + isCommunityEdition ? ( overrideContent ) : ( ), - + isNotHidden(currentCustomer.data.features, 'universes.details.queries') && ( ); diff --git a/managed/ui/src/reducers/reducer_universe.js b/managed/ui/src/reducers/reducer_universe.js index 872d68b4f080..2cf6114f2288 100644 --- a/managed/ui/src/reducers/reducer_universe.js +++ b/managed/ui/src/reducers/reducer_universe.js @@ -59,7 +59,13 @@ import { UPDATE_BACKUP_STATE, UPDATE_BACKUP_STATE_RESPONSE, SET_ALERTS_CONFIG, - SET_ALERTS_CONFIG_RESPONSE + SET_ALERTS_CONFIG_RESPONSE, + CREATE_ALERT_DEFINITION, + CREATE_ALERT_DEFINITION_RESPONSE, + GET_ALERT_DEFINITION, + GET_ALERT_DEFINITION_RESPONSE, + UPDATE_ALERT_DEFINITION, + UPDATE_ALERT_DEFINITION_RESPONSE } from '../actions/universe'; import _ from 'lodash'; import { @@ -103,7 +109,8 @@ const INITIAL_STATE = { healthCheck: getInitialState({}), universeImport: getInitialState({}), alertsConfig: getInitialState({}), - backupState: getInitialState({}) + backupState: getInitialState({}), + alertDefinition: getInitialState({}) }; export default function (state = INITIAL_STATE, action) { @@ -291,7 +298,18 @@ export default function (state = INITIAL_STATE, action) { return { ...state, backupState: getInitialState([]) }; case UPDATE_BACKUP_STATE_RESPONSE: return setPromiseResponse(state, 'backupState', action); - + case CREATE_ALERT_DEFINITION: + return setLoadingState(state, 'alertDefinition', {}); + case CREATE_ALERT_DEFINITION_RESPONSE: + return setPromiseResponse(state, 'alertDefinition', action); + case GET_ALERT_DEFINITION: + return setLoadingState(state, 'alertDefinition', {}); + case GET_ALERT_DEFINITION_RESPONSE: + return setPromiseResponse(state, 'alertDefinition', action); + case UPDATE_ALERT_DEFINITION: + return setLoadingState(state, 'alertDefinition', {}); + case UPDATE_ALERT_DEFINITION_RESPONSE: + return setPromiseResponse(state, 'alertDefinition', action); default: return state; }