package com.intrbiz.bergamot.model;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.intrbiz.bergamot.data.BergamotDB;
import com.intrbiz.bergamot.model.message.AlertMO;
import com.intrbiz.bergamot.model.message.ContactMO;
import com.intrbiz.bergamot.model.message.notification.CheckNotification;
import com.intrbiz.bergamot.model.message.notification.SendAcknowledge;
import com.intrbiz.bergamot.model.message.notification.SendAlert;
import com.intrbiz.bergamot.model.message.notification.SendRecovery;
import com.intrbiz.bergamot.model.state.CheckState;
import com.intrbiz.data.db.compiler.meta.Action;
import com.intrbiz.data.db.compiler.meta.SQLColumn;
import com.intrbiz.data.db.compiler.meta.SQLForeignKey;
import com.intrbiz.data.db.compiler.meta.SQLPrimaryKey;
import com.intrbiz.data.db.compiler.meta.SQLTable;
import com.intrbiz.data.db.compiler.meta.SQLVersion;
/**
* An alert which was raised against a check
*/
@SQLTable(schema = BergamotDB.class, name = "alert", since = @SQLVersion({ 1, 0, 0 }))
public class Alert extends BergamotObject<AlertMO> implements Serializable, Commented
{
private static final long serialVersionUID = 1L;
/**
* The unique ID for this alert
*/
@SQLColumn(index = 1, name = "id", since = @SQLVersion({ 1, 0, 0 }))
@SQLPrimaryKey()
private UUID id;
@SQLColumn(index = 2, name = "site_id", notNull = true, since = @SQLVersion({ 1, 0, 0 }))
@SQLForeignKey(references = Site.class, on = "id", onDelete = Action.CASCADE, onUpdate = Action.RESTRICT, since = @SQLVersion({ 1, 0, 0 }))
private UUID siteId;
/**
* The check to which this alert was issued
*/
@SQLColumn(index = 3, name = "check_id", since = @SQLVersion({ 1, 0, 0 }))
private UUID checkId;
/**
* When was this alert raised
*/
@SQLColumn(index = 4, name = "raised", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp raised;
/**
* Is the check ok?
*/
@SQLColumn(index = 5, name = "ok", since = @SQLVersion({ 1, 0, 0 }))
private boolean ok = true;
/**
* Why is the check ok or not ok?
*/
@SQLColumn(index = 6, name = "status", since = @SQLVersion({ 1, 0, 0 }))
private Status status = Status.PENDING;
/**
* What was the output of the last check
*/
@SQLColumn(index = 7, name = "output", since = @SQLVersion({ 1, 0, 0 }))
private String output = "Pending";
/**
* When did the last check happen
*/
@SQLColumn(index = 8, name = "last_check_time", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp lastCheckTime = new Timestamp(System.currentTimeMillis());
/**
* What was the Id of the last check
*/
@SQLColumn(index = 9, name = "last_check_id", since = @SQLVersion({ 1, 0, 0 }))
private UUID lastCheckId;
/**
* The number of attempts since the last hard state change
*/
@SQLColumn(index = 10, name = "attempt", since = @SQLVersion({ 1, 0, 0 }))
private int attempt = Integer.MAX_VALUE;
/**
* Has a hard state transition happened
*/
@SQLColumn(index = 11, name = "hard", since = @SQLVersion({ 1, 0, 0 }))
private boolean hard = true;
/**
* Is the state in transition
*/
@SQLColumn(index = 12, name = "transitioning", since = @SQLVersion({ 1, 0, 0 }))
private boolean transitioning = false;
/**
* Is the state flapping between ok and not ok, but never reaching a hard state
*/
@SQLColumn(index = 13, name = "flapping", since = @SQLVersion({ 1, 0, 0 }))
private boolean flapping = false;
/**
* When was the last hard state change
*/
@SQLColumn(index = 14, name = "last_state_change", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp lastStateChange = new Timestamp(System.currentTimeMillis());
// history
/**
* A bitmap of the ok history
*/
@SQLColumn(index = 15, name = "ok_history", since = @SQLVersion({ 1, 0, 0 }))
private long okHistory = 0x1L;
/**
* Was the last hard state ok?
*/
@SQLColumn(index = 16, name = "last_hard_ok", since = @SQLVersion({ 1, 0, 0 }))
private boolean lastHardOk = true;
/**
* What was the last hard status?
*/
@SQLColumn(index = 17, name = "last_hard_status", since = @SQLVersion({ 1, 0, 0 }))
private Status lastHardStatus = Status.PENDING;
/**
* What was the output of the last hard state
*/
@SQLColumn(index = 18, name = "last_hard_output", since = @SQLVersion({ 1, 0, 0 }))
private String lastHardOutput = "Pending";
/**
* Has this alert been acknowledged by somebody
*/
@SQLColumn(index = 19, name = "acknowledged", since = @SQLVersion({ 1, 0, 0 }))
private boolean acknowledged = false;
/**
* When was this alert acknowledged
*/
@SQLColumn(index = 20, name = "acknowledged_at", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp acknowledgedAt;
/**
* Whom acknowledged this alert
*/
@SQLColumn(index = 21, name = "acknowledged_by_id", since = @SQLVersion({ 1, 0, 0 }))
@SQLForeignKey(references = Contact.class, on = "id", onDelete = Action.SET_NULL, onUpdate = Action.SET_NULL, since = @SQLVersion({ 1, 0, 0 }))
private UUID acknowledgedById;
/**
* Has this alert recovered by itself
*/
@SQLColumn(index = 22, name = "recovered", since = @SQLVersion({ 1, 0, 0 }))
private boolean recovered = false;
/**
* Which check execution caused this alert to recover
*/
@SQLColumn(index = 23, name = "recovered_by", since = @SQLVersion({ 1, 0, 0 }))
private UUID recoveredBy;
/**
* When did this check recover
*/
@SQLColumn(index = 24, name = "recovered_at", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp recoveredAt;
/**
* Was this alert escalated
*/
@SQLColumn(index = 25, name = "escalated", since = @SQLVersion({ 3, 23, 0 }))
private boolean escalated;
/**
* When was this alert first escalated
*/
@SQLColumn(index = 26, name = "escalated_at", since = @SQLVersion({ 3, 23, 0 }))
private Timestamp escalatedAt;
/**
* Who was notified because of this alert
*/
@SQLColumn(index = 27, name = "notified_ids", type = "UUID[]", since = @SQLVersion({ 3, 26, 0 }))
private List<UUID> notifiedIds = new LinkedList<UUID>();
/**
* The threshold for the next escalation
*/
@SQLColumn(index = 28, name = "escalation_threshold", since = @SQLVersion({ 3, 34, 0 }))
private long escalationThreshold = 0L;
public Alert()
{
super();
}
public Alert(Check<?, ?> check, CheckState state, List<ContactMO> to)
{
this.siteId = check.getSiteId();
this.id = state.getCurrentAlertId();
this.checkId = check.getId();
this.raised = new Timestamp(System.currentTimeMillis());
// copy the state
this.attempt = state.getAttempt();
this.lastCheckId = state.getLastCheckId();
this.lastCheckTime = state.getLastCheckTime();
this.lastHardOutput = state.getLastHardOutput();
this.lastHardStatus = state.getLastHardStatus();
this.lastStateChange = state.getLastStateChange();
this.okHistory = state.getOkHistory();
this.output = state.getOutput();
this.status = state.getStatus();
this.flapping = state.isFlapping();
this.hard = state.isHard();
this.lastHardOk = state.isLastHardOk();
this.ok = state.isOk();
this.transitioning = state.isTransitioning();
// defaults for other state
this.acknowledged = false;
this.acknowledgedAt = null;
this.acknowledgedById = null;
this.recovered = false;
this.recoveredAt = null;
this.recoveredBy = null;
this.escalated = false;
this.escalatedAt = null;
this.notifiedIds = to.stream().map(ContactMO::getId).collect(Collectors.toList());
}
public UUID getSiteId()
{
return siteId;
}
public void setSiteId(UUID siteId)
{
this.siteId = siteId;
}
public UUID getId()
{
return id;
}
public void setId(UUID id)
{
this.id = id;
}
public UUID getCheckId()
{
return checkId;
}
public void setCheckId(UUID checkId)
{
this.checkId = checkId;
}
public Timestamp getRaised()
{
return raised;
}
public void setRaised(Timestamp raised)
{
this.raised = raised;
}
public boolean isOk()
{
return ok;
}
public void setOk(boolean ok)
{
this.ok = ok;
}
public Status getStatus()
{
return status;
}
public void setStatus(Status status)
{
this.status = status;
}
public String getOutput()
{
return output;
}
public void setOutput(String output)
{
this.output = output;
}
public Timestamp getLastCheckTime()
{
return lastCheckTime;
}
public void setLastCheckTime(Timestamp lastCheckTime)
{
this.lastCheckTime = lastCheckTime;
}
public UUID getLastCheckId()
{
return lastCheckId;
}
public void setLastCheckId(UUID lastCheckId)
{
this.lastCheckId = lastCheckId;
}
public int getAttempt()
{
return attempt;
}
public void setAttempt(int attempt)
{
this.attempt = attempt;
}
public boolean isHard()
{
return hard;
}
public void setHard(boolean hard)
{
this.hard = hard;
}
public boolean isTransitioning()
{
return transitioning;
}
public void setTransitioning(boolean transitioning)
{
this.transitioning = transitioning;
}
public boolean isFlapping()
{
return flapping;
}
public void setFlapping(boolean flapping)
{
this.flapping = flapping;
}
public Timestamp getLastStateChange()
{
return lastStateChange;
}
public void setLastStateChange(Timestamp lastStateChange)
{
this.lastStateChange = lastStateChange;
}
public long getOkHistory()
{
return okHistory;
}
public void setOkHistory(long okHistory)
{
this.okHistory = okHistory;
}
public boolean isLastHardOk()
{
return lastHardOk;
}
public void setLastHardOk(boolean lastHardOk)
{
this.lastHardOk = lastHardOk;
}
public Status getLastHardStatus()
{
return lastHardStatus;
}
public void setLastHardStatus(Status lastHardStatus)
{
this.lastHardStatus = lastHardStatus;
}
public String getLastHardOutput()
{
return lastHardOutput;
}
public void setLastHardOutput(String lastHardOutput)
{
this.lastHardOutput = lastHardOutput;
}
public boolean isAcknowledged()
{
return acknowledged;
}
public void setAcknowledged(boolean acknowledged)
{
this.acknowledged = acknowledged;
}
public Timestamp getAcknowledgedAt()
{
return acknowledgedAt;
}
public void setAcknowledgedAt(Timestamp acknowledgedAt)
{
this.acknowledgedAt = acknowledgedAt;
}
public UUID getAcknowledgedById()
{
return acknowledgedById;
}
public void setAcknowledgedById(UUID acknowledgedById)
{
this.acknowledgedById = acknowledgedById;
}
public boolean isRecovered()
{
return recovered;
}
public void setRecovered(boolean recovered)
{
this.recovered = recovered;
}
public UUID getRecoveredBy()
{
return recoveredBy;
}
public void setRecoveredBy(UUID recoveredBy)
{
this.recoveredBy = recoveredBy;
}
public Timestamp getRecoveredAt()
{
return recoveredAt;
}
public void setRecoveredAt(Timestamp recoveredAt)
{
this.recoveredAt = recoveredAt;
}
public boolean isEscalated()
{
return escalated;
}
public void setEscalated(boolean escalated)
{
this.escalated = escalated;
}
public Timestamp getEscalatedAt()
{
return escalatedAt;
}
public void setEscalatedAt(Timestamp escalatedAt)
{
this.escalatedAt = escalatedAt;
}
public List<UUID> getNotifiedIds()
{
return notifiedIds;
}
public void setNotifiedIds(List<UUID> notifiedIds)
{
this.notifiedIds = notifiedIds;
}
/**
* Get comments against this alert
* @param limit the maximum number of comments to get
*/
@Override
public List<Comment> getComments(int limit)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getCommentsForObject(this.getId(), 0, limit);
}
}
/**
* Get comments against this alert
*/
@Override
public List<Comment> getComments()
{
return this.getComments(5);
}
/**
* The check
*/
public Check<?,?> getCheck()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getCheck(this.getCheckId());
}
}
public Contact getAcknowledgedBy()
{
if (this.getAcknowledgedById() == null) return null;
try (BergamotDB db = BergamotDB.connect())
{
return db.getContact(this.getAcknowledgedById());
}
}
public List<Contact> getNotified()
{
try (BergamotDB db = BergamotDB.connect())
{
return this.getNotifiedIds().stream().map((id) -> db.getContact(id)).filter((c) -> c != null).collect(Collectors.toList());
}
}
public List<AlertEscalation> getEscalations()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getAlertEscalations(this.getId());
}
}
public List<AlertEncompasses> getEncompasses()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getAlertEncompasses(this.getId());
}
}
public long getEscalationThreshold()
{
return escalationThreshold;
}
public void setEscalationThreshold(long escalationThreshold)
{
this.escalationThreshold = escalationThreshold;
}
@Override
public AlertMO toMO(Contact contact, EnumSet<MOFlag> options)
{
AlertMO mo = new AlertMO();
mo.setAcknowledged(this.isAcknowledged());
mo.setAcknowledgedAt(this.getAcknowledgedAt() == null ? -1 : this.getAcknowledgedAt().getTime());
Contact ackedBy = this.getAcknowledgedBy();
if (ackedBy != null && (contact == null || contact.hasPermission("read", ackedBy))) mo.setAcknowledgedBy(ackedBy.toStubMO(contact));
mo.setCheck(this.getCheck().toMO(contact));
mo.setFlapping(this.isFlapping());
mo.setHard(this.isHard());
mo.setId(this.getId());
mo.setLastCheckId(this.getLastCheckId());
mo.setLastCheckTime(this.getLastCheckTime() == null ? -1 : this.getLastCheckTime().getTime());
mo.setLastHardOk(this.isLastHardOk());
mo.setLastHardOutput(this.getLastHardOutput());
mo.setLastHardStatus(this.getLastHardStatus().toString());
mo.setLastStateChange(this.getLastStateChange() == null ? -1 : this.getLastStateChange().getTime());
mo.setOk(this.isOk());
mo.setOutput(this.getOutput());
mo.setRaised(this.getRaised().getTime());
mo.setRecovered(this.isRecovered());
mo.setRecoveredAt(this.getRecoveredAt() == null ? -1 : this.getRecoveredAt().getTime());
mo.setRecoveredBy(this.getRecoveredBy());
mo.setStatus(this.getStatus().toString());
mo.setTransitioning(this.isTransitioning());
if (options.contains(MOFlag.COMMENTS))
{
mo.setComments(this.getComments().stream().map((x) -> x.toMO(contact)).collect(Collectors.toList()));
}
mo.setEscalated(this.isEscalated());
mo.setEscalatedAt(this.getEscalatedAt() == null ? -1 : this.getEscalatedAt().getTime());
mo.setNotified(this.getNotified().stream().filter((c) -> contact == null || contact.hasPermission("read", c)).map((c) -> c.toStubMO(contact)).collect(Collectors.toList()));
mo.setEscalations(this.getEscalations().stream().map((e) -> e.toStubMO(contact)).collect(Collectors.toList()));
mo.setEncompassed(this.getEncompasses().stream().map((e) -> e.toStubMO(contact)).collect(Collectors.toList()));
return mo;
}
/**
* Construct a generic notification based of this alert.
*
* TODO: rebase the notification messages on this alert object
*
* @param now when the alert is raise (ie: now)
* @param type the notification type: alert, recovery, acknowledge
* @param ctor the constructor for the notification type
* @return the notification
*/
public <T extends CheckNotification> T createNotification(Calendar now, NotificationType type, Supplier<T> ctor, List<ContactMO> to)
{
Check<?,?> check = this.getCheck();
CheckState state = check.getState();
// check if to send
if (! check.getNotifications().isEnabledAt(type, state.getStatus(), now)) return null;
// create the notifications
T notification = ctor.get();
// the site
notification.setSite(check.getSite().toMOUnsafe());
notification.setRaised(now.getTimeInMillis());
// alert id
notification.setAlertId(this.getId());
// send
notification.setRaised(System.currentTimeMillis());
notification.setCheck(check.toMOUnsafe());
// to
notification.setTo(to);
return notification;
}
public SendAlert createAlertNotification(Calendar now, List<ContactMO> to)
{
return this.createNotification(now, NotificationType.ALERT, SendAlert::new, to);
}
public SendAlert createEscalatedAlertNotification(Calendar now, List<ContactMO> to)
{
SendAlert sa = this.createNotification(now, NotificationType.ALERT, SendAlert::new, to);
sa.setEscalation(true);
return sa;
}
public SendRecovery createRecoveryNotification(Calendar now)
{
List<ContactMO> to = this.getContactsToNotify(this.getCheck(), this.getCheck().getState().getStatus(), now);
return this.createNotification(now, NotificationType.RECOVERY, SendRecovery::new, to);
}
public SendAcknowledge createAcknowledgeNotification(Calendar now, Contact acknowledgedBy, Comment acknowledgeComment)
{
List<ContactMO> to = this.getContactsToNotify(this.getCheck(), this.getCheck().getState().getStatus(), now);
SendAcknowledge sa = this.createNotification(now, NotificationType.ACKNOWLEDGE, SendAcknowledge::new, to);
if (sa == null) return null;
// additional detail for acknowledge
sa.setAcknowledgedBy(acknowledgedBy.toStubMOUnsafe());
sa.setAcknowledgeSummary(acknowledgeComment.getSummary());
sa.setAcknowledgeComment(acknowledgeComment.getComment());
// done
return sa;
}
/**
* Compute the list of contacts who should be notified on recovery or acknowledgement
* @return a list of contact message objects
*/
public List<ContactMO> getContactsToNotify(Check<?,?> check, Status status, Calendar time)
{
final Notifications checkNotifications = check.getNotifications();
// compute the engines available
final Set<String> enabledEngines = checkNotifications.getEnginesEnabledAt(NotificationType.ALERT, status, time);
// compute the contacts to notify
return this.getNotified().stream()
.filter((contact) -> contact.getNotifications().isEnabledAt(NotificationType.ALERT, status, time))
.map((contact) -> {
ContactMO cmo = contact.toMOUnsafe();
cmo.setEngines(
contact.getNotifications().getEnginesEnabledAt(NotificationType.ALERT, status, time).stream()
.filter((engine) -> checkNotifications.isAllEnginesEnabled() || enabledEngines.contains(engine))
.collect(Collectors.toSet())
);
return cmo;
}).collect(Collectors.toList());
}
public List<ContactMO> getContactsToNotify(Calendar time)
{
return this.getContactsToNotify(this.getCheck(), this.getCheck().getState().getStatus(), time);
}
}