Files
houston-be/model/incident/incident.go
Vijay Joshi 7a559462a4 NTP-22602 : Disable sla breach message and auto escalation in sev-1 to sev-0 cases (#469)
* NTP-22602 : Disable sla breach message and auto escalation in sev-1 to sev-0 cases

* NTP-22602 : Make env vars for severities

* NTP-22602 : Query fix

* NTP-22602 : Add to application.properties file

---------

Co-authored-by: Vijay Joshi <ee-automation@navi.com>
2024-12-26 13:52:29 +05:30

752 lines
22 KiB
Go

package incident
import (
"encoding/json"
"fmt"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
"go.uber.org/zap"
"houston/logger"
"houston/model/log"
"houston/model/product"
"houston/model/team"
"houston/service/incidentStatus"
"houston/service/severity"
utils "houston/service/utils"
"strconv"
"strings"
"time"
"gorm.io/gorm"
)
type Repository struct {
gormClient *gorm.DB
logRepository *log.Repository
teamRepository *team.Repository
socketModeClient *socketmode.Client
severityService severity.ISeverityService
incidentStatusService incidentStatus.IncidentStatusService
}
var valueBeforeUpdate IncidentEntity
var valueAfterUpdate IncidentEntity
var valueBeforeCreate IncidentEntity
var valueAfterCreate IncidentEntity
var differences []utils.Difference
func NewIncidentRepository(
gormClient *gorm.DB,
severityService severity.ISeverityService,
incidentStatusService incidentStatus.IncidentStatusService,
logRepository *log.Repository,
teamRepository *team.Repository,
socketModeClient *socketmode.Client,
) *Repository {
return &Repository{
gormClient: gormClient,
severityService: severityService,
incidentStatusService: incidentStatusService,
logRepository: logRepository,
teamRepository: teamRepository,
socketModeClient: socketModeClient,
}
}
func (r *Repository) CreateIncidentEntity(request *CreateIncidentDTO, tx *gorm.DB) (*IncidentEntity, error) {
severityId, err := strconv.Atoi(request.Severity)
if err != nil {
return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err)
}
severity, err := r.severityService.FindSeverityById(uint(severityId))
if err != nil {
return nil, fmt.Errorf("fetch FindSeverityById failed. err: %v", err)
}
teamId, _ := strconv.Atoi(request.TeamId)
incidentStatus, _ := r.incidentStatusService.GetIncidentStatusByStatusName(string(request.Status))
var products = make([]product.ProductEntity, 0)
for _, productId := range request.ProductIds {
products = append(products, product.ProductEntity{ID: productId})
}
incidentEntity := &IncidentEntity{
Title: request.Title,
Description: request.Description,
Status: incidentStatus.ID,
SeverityId: uint(severityId),
DetectionTime: request.DetectionTime,
StartTime: request.StartTime,
TeamId: uint(teamId),
EnableReminder: request.EnableReminder,
SeverityTat: time.Now().AddDate(0, 0, severity.Sla),
CreatedBy: request.CreatedBy,
UpdatedBy: request.UpdatedBy,
MetaData: request.MetaData,
ReportingTeamId: request.ReportingTeamID,
Products: products,
IsPrivate: request.IsPrivate,
}
tx.Create(incidentEntity)
return incidentEntity, nil
}
func (i *IncidentEntity) AfterCreate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("AfterUpdate executed at: %v", time.Now()))
valueBeforeCreate = IncidentEntity{}
valueAfterCreate = IncidentEntity{}
if err := tx.First(&valueAfterCreate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity after created is: %s", valueAfterCreate))
differences = utils.DeepCompare(valueBeforeCreate, valueAfterCreate)
return nil
}
func (i *IncidentEntity) BeforeUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("BeforeUpdate executed at: %v", time.Now()))
valueBeforeUpdate = IncidentEntity{}
if err := tx.First(&valueBeforeUpdate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity before update is: %s", valueBeforeUpdate))
return nil
}
func (i *IncidentEntity) AfterUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("AfterUpdate executed at: %v", time.Now()))
valueAfterUpdate = IncidentEntity{}
if err := tx.First(&valueAfterUpdate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity after updated is: %s", valueAfterUpdate))
differences = append(differences, utils.DeepCompare(valueBeforeUpdate, valueAfterUpdate)...)
return nil
}
func (r *Repository) processDiffIds() []byte {
var jsonDiff []byte
for index := range differences {
switch differences[index].Attribute {
case "Status":
if differences[index].From != "" {
statusIdString, _ := strconv.Atoi(differences[index].From)
fromStatus, _ := r.incidentStatusService.GetIncidentStatusByStatusId(uint(statusIdString))
if fromStatus != nil {
differences[index].From = fromStatus.Name
}
}
statusIdString, _ := strconv.Atoi(differences[index].To)
toStatus, _ := r.incidentStatusService.GetIncidentStatusByStatusId(uint(statusIdString))
differences[index].To = toStatus.Name
case "SeverityId":
if differences[index].From != "" {
severityIdString, _ := strconv.Atoi(differences[index].From)
severityEntity, _ := r.severityService.FindSeverityById(uint(severityIdString))
if severityEntity != nil {
differences[index].From = severityEntity.Name
}
}
severityIdString, _ := strconv.Atoi(differences[index].To)
severityEntity, _ := r.severityService.FindSeverityById(uint(severityIdString))
differences[index].To = severityEntity.Name
case "TeamId":
if differences[index].From != "" {
teamIdString, _ := strconv.Atoi(differences[index].From)
teamEntity, _ := r.teamRepository.FindTeamById(uint(teamIdString))
if teamEntity != nil {
differences[index].From = teamEntity.Name
}
}
teamIdString, _ := strconv.Atoi(differences[index].To)
teamEntity, _ := r.teamRepository.FindTeamById(uint(teamIdString))
if teamEntity != nil {
differences[index].To = teamEntity.Name
}
}
}
jsonDiff, _ = json.Marshal(differences)
return jsonDiff
}
func (r *Repository) processUserInfo() ([]byte, error) {
user, err := r.socketModeClient.GetUsersInfo(valueAfterUpdate.UpdatedBy)
if err != nil {
errorMessage := fmt.Sprintf("failed to get user info from slack for userID: %s", valueAfterUpdate.UpdatedBy)
logger.Error(errorMessage)
return nil, fmt.Errorf("%s. Error: %v", errorMessage, err)
}
if len(*user) != 0 {
userData := (*user)[0]
userInfo := log.UserInfo{
Id: userData.ID,
Email: userData.Profile.Email,
Name: userData.Profile.RealName,
}
return json.Marshal(userInfo)
}
return nil, fmt.Errorf("%s is not a valid user", valueAfterUpdate.UpdatedBy)
}
func (r *Repository) captureLogs(justification string) {
if differences != nil && len(differences) > 0 {
jsonUser, _ := r.processUserInfo()
jsonDiff := r.processDiffIds()
logEntity := log.LogEntity{
CreatedAt: time.Now(),
RelationName: "incident",
RecordId: valueAfterUpdate.ID,
UserInfo: jsonUser,
Changes: jsonDiff,
}
if justification != "" {
logEntity.Justification = justification
}
_, err := r.logRepository.CreateLog(&logEntity)
if err != nil {
logger.Error(fmt.Sprintf("%d failed to create log. Error: %v", logEntity.RecordId, err))
}
differences = []utils.Difference{}
}
}
func (r *Repository) UpdateIncident(incidentEntity *IncidentEntity) error {
result := r.gormClient.Updates(incidentEntity)
if result.Error != nil {
return result.Error
}
r.captureLogs("")
return nil
}
func (r *Repository) UpdateIncidentWithAssociations(incidentEntity *IncidentEntity) error {
err := r.gormClient.Model(&incidentEntity).Association("Products").Replace(incidentEntity.Products)
if err != nil {
return err
}
result := r.gormClient.Updates(incidentEntity)
if result.Error != nil {
return result.Error
}
r.captureLogs("")
return nil
}
func (r *Repository) UpdateIncidentWithJustification(incidentEntity *IncidentEntity, justification string) error {
result := r.gormClient.Updates(incidentEntity)
if result.Error != nil {
return result.Error
}
r.captureLogs(justification)
return nil
}
func (r *Repository) FindIncidentByChannelId(channelId string) (*IncidentEntity, error) {
var incidentEntity IncidentEntity
result := r.gormClient.Preload("Products").Preload("ReportingTeam").Find(&incidentEntity, "slack_channel = ?", channelId)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &incidentEntity, nil
}
func (r *Repository) FindIncidentById(Id uint) (*IncidentEntity, error) {
var incidentEntity IncidentEntity
result := r.gormClient.Preload("Products").Preload("ReportingTeam").Find(&incidentEntity, "id = ?", Id)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, fmt.Errorf("could not find incident with ID: %d", Id)
}
return &incidentEntity, nil
}
func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool, isArchived *bool) (*[]IncidentEntity, int, error) {
var query = r.gormClient.Model([]IncidentEntity{})
var incidentEntity []IncidentEntity
if isArchived != nil {
query = query.Joins(
"JOIN incident_channel on incident.slack_channel = incident_channel.slack_channel "+
"and incident_channel.is_archived = ?",
isArchived,
)
}
if len(teamsIds) != 0 {
query = query.Where("team_id IN ?", teamsIds)
}
if len(severityIds) != 0 {
query = query.Where("severity_id IN ?", severityIds)
}
if len(statusIds) != 0 {
query = query.Where("status IN ?", statusIds)
}
if isPrivate != nil {
query = query.Where("is_private = ?", isPrivate)
}
var totalElements int64
result := query.Count(&totalElements)
if result.Error != nil {
return nil, 0, result.Error
}
if result.RowsAffected == 0 {
return nil, int(totalElements), nil
}
result = query.Order("created_at desc").Preload("Team").Preload("Severity").Preload("ReportingTeam").Find(&incidentEntity)
if result.Error != nil {
return nil, 0, result.Error
}
if result.RowsAffected == 0 {
return nil, int(totalElements), nil
}
return &incidentEntity, int(totalElements), nil
}
func (r *Repository) FetchAllIncidentsPaginated(
productIds []uint,
reportingTeamIds []uint,
TeamsId []uint,
SeverityIds []uint,
StatusIds []uint,
pageNumber int64,
pageSize int64,
incidentName string,
from string,
to string,
userId *uint,
) ([]IncidentEntity, int, error) {
var query = r.gormClient.Model([]IncidentEntity{}).Preload("Products")
var incidentEntity []IncidentEntity
if len(TeamsId) != 0 {
query = query.Where("team_id IN ?", TeamsId)
}
if len(reportingTeamIds) != 0 {
query = query.Where("reporting_team_id IN ?", reportingTeamIds)
}
if len(SeverityIds) != 0 {
query = query.Where("severity_id IN ?", SeverityIds)
}
if len(StatusIds) != 0 {
query = query.Where("status IN ?", StatusIds)
}
if len(strings.TrimSpace(incidentName)) != 0 {
query = query.Where("incident_name LIKE ?", "%"+incidentName+"%")
}
if len(from) != 0 {
query = query.Where("created_at >= ?", from)
}
if len(to) != 0 {
query = query.Where("created_at <= ?", to)
}
if len(productIds) != 0 {
query = query.
Joins("JOIN incident_products ON incident_products.incident_entity_id = incident.id").
Where("incident_products.product_entity_id IN ?", productIds).
Group("incident.id")
}
if userId != nil {
query = query.Where(
fmt.Sprintf("%s OR %s or %s", isPrivateCondition, incidentTeamsAccessCondition, userInIncidentCondition),
false, *userId, *userId,
)
} else {
query = query.Where("is_private = ?", false)
}
var totalElements int64
result := query.Count(&totalElements)
if result.Error != nil {
return nil, 0, result.Error
}
result = query.Order("created_at desc").Offset(int(pageNumber * pageSize)).Limit(int(pageSize)).Find(&incidentEntity)
if result.Error != nil {
return nil, 0, result.Error
}
if result.RowsAffected == 0 {
return nil, int(totalElements), nil
}
return incidentEntity, int(totalElements), nil
}
func (r *Repository) UpsertIncidentRole(addIncidentRoleRequest *AddIncidentRoleRequest) error {
incidentRolesEntity := &IncidentRoleEntity{
IncidentId: addIncidentRoleRequest.IncidentId,
Role: addIncidentRoleRequest.Role,
AssignedTo: addIncidentRoleRequest.UserId,
AssignedBy: addIncidentRoleRequest.CreatedById,
}
var incidentRole IncidentRoleEntity
result := r.gormClient.Find(&incidentRole, "incident_id = ? AND role = ?", addIncidentRoleRequest.IncidentId, addIncidentRoleRequest.Role)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 1 {
incidentRolesEntity.ID = incidentRole.ID
incidentRolesEntity.CreatedAt = incidentRole.CreatedAt
incidentRolesEntity.UpdatedAt = time.Now()
addResult := r.gormClient.Save(incidentRolesEntity)
if addResult != nil {
return addResult.Error
}
return nil
} else if result.RowsAffected == 0 {
addResult := r.gormClient.Create(incidentRolesEntity)
if addResult != nil {
return addResult.Error
}
return nil
}
return gorm.ErrInvalidData
}
func (r *Repository) CreateIncidentChannelEntry(request *CreateIncidentChannelEntry) error {
messageEntity := &IncidentChannelEntity{
SlackChannel: request.SlackChannel,
MessageTimeStamp: request.MessageTimeStamp,
IncidentId: request.IncidentId,
}
result := r.gormClient.Create(&messageEntity)
if result.Error != nil {
return result.Error
}
return nil
}
func (r *Repository) CreateIncidentTag(incidentId, tagId uint) (*IncidentTagEntity, error) {
incidentTag := IncidentTagEntity{
IncidentId: incidentId,
TagId: tagId,
}
result := r.gormClient.Create(&incidentTag)
if result.Error != nil {
return nil, result.Error
}
return &incidentTag, nil
}
func (r *Repository) CreateIncidentTagsInBatchesForAnIncident(incidentId uint, tagIds []uint) (*[]IncidentTagEntity, error) {
var incidentTags []IncidentTagEntity
for _, tagId := range tagIds {
incidentTag := IncidentTagEntity{
IncidentId: incidentId,
TagId: tagId,
}
incidentTags = append(incidentTags, incidentTag)
}
result := r.gormClient.CreateInBatches(&incidentTags, len(incidentTags))
if result.Error != nil {
return nil, result.Error
}
return &incidentTags, nil
}
func (r *Repository) GetIncidentChannels(incidentId uint) (*[]IncidentChannelEntity, error) {
var incidentChannels []IncidentChannelEntity
result := r.gormClient.Find(&incidentChannels, "incident_id = ?", incidentId)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &incidentChannels, nil
}
func (r *Repository) GetIncidentTagsByIncidentId(incidentId uint) (*[]IncidentTagEntity, error) {
var incidentTags []IncidentTagEntity
result := r.gormClient.Find(&incidentTags, "incident_id = ?", incidentId)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, nil
}
return &incidentTags, nil
}
func (r *Repository) GetIncidentTagByTagId(incidentId uint, tagId uint) (*IncidentTagEntity, error) {
var incidentTag IncidentTagEntity
result := r.gormClient.Find(&incidentTag, "incident_id = ? and tag_id = ?", incidentId, tagId)
if result.Error != nil {
return nil, result.Error
} else if result.RowsAffected == 0 {
return nil, nil
}
return &incidentTag, nil
}
func (r *Repository) GetIncidentTagsByTagIds(incidentId uint, tagIds []uint) (*IncidentTagEntity, error) {
var incidentTag IncidentTagEntity
result := r.gormClient.Find(&incidentTag, "incident_id = ? and tag_id in ?", incidentId, tagIds)
if result.Error != nil {
return nil, result.Error
} else if result.RowsAffected == 0 {
return nil, nil
}
return &incidentTag, nil
}
func (r *Repository) SaveIncidentTag(entity IncidentTagEntity) (*IncidentTagEntity, error) {
tx := r.gormClient.Save(&entity)
if tx.Error != nil {
return nil, tx.Error
}
return &entity, nil
}
func (r *Repository) GetIncidentsForEscalation() ([]IncidentEntity, error) {
var incidentEntity []IncidentEntity
currentTime := time.Now()
result := r.gormClient.Find(&incidentEntity,
fmt.Sprintf("status IN (?) AND severity_id NOT IN %s AND severity_tat <= ?", viper.GetString("severities.excluded.for.escalation")),
statusesForEscalation, currentTime)
if result.Error != nil {
return nil, result.Error
}
return incidentEntity, nil
}
func (r *Repository) GetIncidentsForMovingToInvestigating() ([]IncidentEntity, error) {
var incidentEntities []IncidentEntity
rawTeamIDs := viper.GetString("investigation.reopen.ignore.team_ids")
var teamIDsToIgnore []int
for _, str := range strings.Split(rawTeamIDs, ",") {
teamID, err := strconv.Atoi(strings.TrimSpace(str))
if err != nil {
logger.Error(fmt.Sprintf("Error converting value to integer: %v", err))
}
teamIDsToIgnore = append(teamIDsToIgnore, teamID)
}
incidentInvestigationReopenDays := viper.GetInt("investigation.reopen.threshold.days")
dateBefore := time.Now().AddDate(0, 0, -incidentInvestigationReopenDays).Format(time.RFC3339)
result := r.gormClient.Raw(incidentsToMoveToInvestigating, teamIDsToIgnore, dateBefore).Scan(&incidentEntities)
if result.Error != nil {
return nil, result.Error
}
return incidentEntities, nil
}
func (r *Repository) GetIncidentsByTeamIdAndNotResolvedAndOfSev0OrSev1OrSev2(team_id uint) (*[]IncidentEntity, error) {
var incidentEntity []IncidentEntity
result := r.gormClient.Order("severity_id").Order("created_at desc").Find(&incidentEntity, "team_id = ? AND status NOT IN ? AND severity_id IN ?", team_id, []uint{4, 5}, []uint{1, 2, 3})
if result.Error != nil {
return nil, result.Error
}
return &incidentEntity, nil
}
func (r *Repository) GetCountOfIncidentsByTeamIdAndNotResolved(team_id uint) (int, error) {
var incidentEntity []IncidentEntity
result := r.gormClient.Find(&incidentEntity, "team_id = ? AND status NOT IN ? AND severity_id IN (1,2,3)", team_id, []uint{4, 5})
if result.Error != nil {
return 0, result.Error
}
return int(result.RowsAffected), nil
}
func (r *Repository) GetIncidentRoleByIncidentIdAndRole(incident_id uint, role string) (*IncidentRoleEntity, error) {
var incidentRoleEntity IncidentRoleEntity
result := r.gormClient.Find(&incidentRoleEntity, "incident_id = ? and role = ? and deleted_at IS NULL", incident_id, role)
if result.Error != nil {
return nil, result.Error
}
return &incidentRoleEntity, nil
}
func (r *Repository) GetIncidentRolesByIncidentIdsAndRole(incidentsIds []uint, role string) ([]IncidentRoleEntity, error) {
var incidentRoleEntity []IncidentRoleEntity
result := r.gormClient.Find(&incidentRoleEntity, "incident_id IN ? and role = ? and deleted_at IS NULL", incidentsIds, role)
if result.Error != nil {
return nil, result.Error
}
return incidentRoleEntity, nil
}
func (r *Repository) GetOpenIncidentsByCreatorIdForGivenTeamAndStatuses(created_by string, teamId uint, statusIds []uint) (*[]IncidentEntity, error) {
var incidentEntities []IncidentEntity
result := r.gormClient.Find(&incidentEntities, "created_by = ? AND team_id = ? AND status IN ?", created_by, teamId, statusIds)
if result.Error != nil {
logger.Error(fmt.Sprintf("Error in fetching open incidents created by %s and for team %d", created_by, teamId), zap.Error(result.Error))
return nil, result.Error
}
return &incidentEntities, nil
}
func (r *Repository) FindOpenIncidentsByTeamOrderedByCreationTimeAndSeverity(team string) (*[]IncidentEntity, error) {
var incidentEntity []IncidentEntity
query := fmt.Sprintf("SELECT * FROM incident WHERE team_id = %v AND status NOT IN (4, 5) AND is_private = false AND deleted_at IS NULL ORDER BY severity_id, id", team)
result :=
r.gormClient.Raw(query).Scan(&incidentEntity)
if result.Error != nil {
return nil, result.Error
}
return &incidentEntity, nil
}
func (r *Repository) FetchIncidentsWithSeverityTatBetweenGivenRange(slaStart, slaEnd string) (*[]IncidentEntity, error) {
var incidents []IncidentEntity
query := r.gormClient.Where(
fmt.Sprintf("severity_tat >= ? AND severity_tat < ? AND severity_id IN %s AND status IN (?)", viper.GetString("severities.for.sla.breach")),
slaStart, slaEnd, statusesForEscalation).
Order("team_id").Preload("Team").Preload("Severity").Find(&incidents)
if query.Error != nil {
return nil, query.Error
}
return &incidents, nil
}
func (r *Repository) UpdateIncidentChannelEntity(incidentChannelEntity *IncidentChannelEntity) error {
result := r.gormClient.Select("*").Updates(incidentChannelEntity)
if result.Error != nil {
return result.Error
}
return nil
}
func (r *Repository) CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) (bool, error) {
var count int64
query := r.gormClient.Table("incident").Joins("JOIN houston_user ON houston_user.email = ?", userEmail)
query = query.Where("incident.id = ?", incidentId)
query = query.Where(
fmt.Sprintf("%s OR %s or %s", isPrivateCondition, incidentTeamsAccessCondition, userInIncidentCondition),
false, gorm.Expr("houston_user.id"), gorm.Expr("houston_user.id"),
)
if err := query.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *Repository) GetIncidentsByIds(incidentIds []uint) ([]IncidentEntity, error) {
var incidents []IncidentEntity
query := r.gormClient.Where("id IN ?", incidentIds).Find(&incidents)
if query.Error != nil {
return nil, query.Error
}
return incidents, nil
}
var statusesForEscalation = []uint{1, 2}
const isPrivateCondition = "is_private = ?"
const incidentTeamsAccessCondition = `EXISTS (
SELECT 1
FROM team_user
WHERE team_user.user_id = ?
AND (
team_user.team_id = incident.team_id
OR team_user.team_id = incident.reporting_team_id
)
)`
const userInIncidentCondition = `EXISTS (
SELECT 1
FROM incident_user
WHERE incident_user.incident_id = incident.id
AND incident_user.user_id = ?
)`
const incidentsToMoveToInvestigating = `
WITH incidents_in_monitoring_or_identified_stage AS (
SELECT *
FROM incident i
WHERE i.status IN (
SELECT ists.id
FROM incident_status ists
WHERE ists.name IN ('Monitoring', 'Identified')
) AND i.team_id NOT IN ?
),
max_log_entries AS (
SELECT
record_id,
MAX(id) AS max_id
FROM log
WHERE relation_name = 'incident'
AND log.created_at < ?
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(log.changes) AS change
WHERE change->>'attribute' = 'Status'
AND change->>'to' IN ('Monitoring', 'Identified')
)
GROUP BY record_id
)
SELECT imi.*
FROM log l
JOIN max_log_entries m ON l.record_id = m.record_id AND l.id = m.max_id
JOIN incidents_in_monitoring_or_identified_stage as imi on imi.id = l.record_id;
`