diff --git a/cmd/cmd b/cmd/cmd new file mode 100755 index 0000000..c26a1f5 Binary files /dev/null and b/cmd/cmd differ diff --git a/common/util/common_util.go b/common/util/common_util.go index 7266216..622b89d 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -20,6 +20,19 @@ func GetColorBySeverity(severityId uint) string { } } +func GetColourByOpenIncidents(openIncidents int) string { + switch { + case openIncidents <= 5: + return "#50C878" + case openIncidents <= 10: + return "#FDDA0D" + case openIncidents > 10: + return "#FF0000" + default: + return "#808080" + } +} + func PostIncidentStatusUpdateMessage(userId, updatedStatus, channelId string, client *socketmode.Client) error { msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> > set status to %s", userId, updatedStatus), false) _, _, errMessage := client.PostMessage(channelId, msgOption) @@ -54,4 +67,4 @@ func RemoveDuplicateStr(strSlice []string) []string { } } return list -} +} \ No newline at end of file diff --git a/config/application.properties b/config/application.properties index 2b35365..de65d3f 100644 --- a/config/application.properties +++ b/config/application.properties @@ -18,6 +18,9 @@ cron.job.update.incident.interval=CRON_JOB_UPDATE_INCIDENT_INTERVAL cron.job.lock.default.time.in.sec=CRON_JOB_LOCK_DEFAULT_TIME_IN_SEC cron.job.lock.ticker.time.in.sec=CRON_JOB_LOCK_TICKER_TIME_IN_SEC cron.job.name=CRON_JOB_NAME +cron.job.team.metric=CRON_JOB_TEAM_METRIC +cron.job.team.metric.interval=CRON_JOB_TEAM_METRIC_INTERVAL +team.metric.update.channel=TEAM_METRIC_UPDATE_CHANNEL_ID #incidents incidents.show.limit=INCIDENTS_SHOW_LIMIT diff --git a/internal/cron/cron.go b/internal/cron/cron.go index 3a16cfa..d2c393c 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "houston/common/util" "houston/internal/processor/action" "houston/model/incident" "houston/model/severity" @@ -21,14 +22,24 @@ import ( func RunJob(socketModeClient *socketmode.Client, db *gorm.DB, logger *zap.Logger, incidentService *incident.Repository, severityService *severity.Repository, teamService *team.Repository, shedlockService *shedlock.Repository) { + //HOUSTON ESCALATE shedlockConfig := NewLockerDbWithLockTime(viper.GetInt("cron.job.lock.default.time.in.sec")) err := shedlockConfig.AddFun(viper.GetString("cron.job.name"), viper.GetString("cron.job.update.incident.interval"), shedlockService, func() { UpdateIncidentByCronJob(socketModeClient, db, logger, incidentService, teamService, severityService, viper.GetString("cron.job.name")) }) if err != nil { - logger.Error(err.Error()) + logger.Error("HOUSTON_ESCALATE error: " + err.Error()) } + + //HOUSTON DAILY TEAM UPDATE + err = shedlockConfig.AddFun(viper.GetString("cron.job.team.metric"), viper.GetString("cron.job.team.metric.interval"), shedlockService, func() { + postTeamMetrics(socketModeClient, db, logger, incidentService, teamService, severityService, viper.GetString("cron.job.team.metric")) + }) + if err != nil { + logger.Error("HOUSTON_TEAM_METRICS error :" + err.Error()) + } + shedlockConfig.Start() } @@ -128,3 +139,142 @@ func updatingSevForEachInc(logger *zap.Logger, incidents []incident.IncidentEnti logger.Error("PostMessage failed for cronJob ", zap.Error(errMessage), zap.Int("incidentId", int(incidents[i].ID))) } } + +func postTeamMetrics(socketModeClient *socketmode.Client, db *gorm.DB, logger *zap.Logger, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, name string) { + fmt.Println("Running Team Metrics cron at", time.Now().Format(time.RFC3339)) + defer func() { + if r := recover(); r != nil { + logger.Error(fmt.Sprintf("Exception occurred in cron: %v", r.(error))) + } + }() + + teams, err := teamService.GetAllActiveTeams() + if err != nil { + logger.Error("GetAllActiveTeams error", zap.Error(err)) + return + } + + teamsList := *teams + //Do not need to fetch Severity every time, hence keeping a map outside. + incidentSeverityList, err := severityService.GetAllActiveSeverity() + if err != nil || incidentSeverityList == nil { + logger.Error("GetAllActiveSeverity error in cron Job", + zap.String("cron_name", name), zap.Error(err)) + return + } + severityMap := convertSeverityListToMap(*incidentSeverityList) + + for i := 0; i < len(teamsList); i++ { + incidents, err := incidentService.GetIncidentsByTeamIdAndNotResolved(teamsList[i].ID) + if err != nil { + logger.Error("GetIncidentsByTeamIdAndNotResolved error in cron Job", + zap.String("cron_name", name), zap.Error(err)) + continue + } + incidentsList := *incidents + + list := make([]incident.SlackChannelWithResponderId, 0, len(incidentsList)) + for j := 0; j < len(incidentsList); j++ { + //RESPONDER + incidentRole, err := incidentService.GetIncidentRoleByIncidentIdAndRole(incidentsList[j].ID, incident.Responder) + if err != nil { + logger.Error("GetIncidentRoleByIncidentIdAndRole error in cron Job", + zap.String("cron_name", name), zap.Error(err)) + continue + } + + // map to pojo and to list + obj := incident.SlackChannelWithResponderId{ + SlackChannel: incidentsList[j].SlackChannel, + ResponderId: incidentRole.AssignedTo, + CreatedAt: incidentsList[j].CreatedAt, + Severity: severityMap[incidentsList[j].SeverityId], + ManagerId: teamsList[i].ManagerHandle, + } + // Appending the obj to the list + list = append(list, obj) + } + //Post message + teamName := builderTeamNameHeader(teamsList[i].Name) + openIncidentCount := buildOpenIncidentText(len(incidentsList)) + onCallAndManagerBlock := buildOnCallAndManagerBlock(teamsList[i].OncallHandle, teamsList[i].ManagerHandle) + blocks := IncidentMetricBlock(teamName, openIncidentCount, onCallAndManagerBlock) + //On the basis of no of incidents, change colour + color := util.GetColourByOpenIncidents(len(incidentsList)) + att := slack.Attachment{Blocks: blocks, Color: color} + _, _, err = socketModeClient.PostMessage(viper.GetString("team.metric.update.channel"), slack.MsgOptionAttachments(att)) + + text := "" + for index := 0; index < len(list); index++ { + // Calculate the time difference + currentTime := time.Now() + duration := 5*time.Hour + 30*time.Minute + newTime := currentTime.Add(duration) + timeDiff := newTime.Sub(list[index].CreatedAt) + + // Convert the duration into days and hours + days := int(timeDiff.Hours() / 24) + hours := int(timeDiff.Hours()) % 24 + if list[index].ResponderId == "" && list[index].ManagerId != "" { + text += fmt.Sprintf("<#%s>'s *Severity* is `%s` assigned to *Manager* <@%s>. *Open Since*- `%dd%dhr` \n", list[index].SlackChannel, list[index].Severity, list[index].ManagerId, days, hours) + } else if list[index].ResponderId != "" { + text += fmt.Sprintf("<#%s>'s *Severity* is `%s` assigned to *Responder* <@%s>. *Open Since*- `%dd%dhr` \n", list[index].SlackChannel, list[index].Severity, list[index].ResponderId, days, hours) + } else if list[index].ResponderId == "" && list[index].ManagerId == "" { + text += fmt.Sprintf("<#%s>'s *Severity* is `%s` assigned to *No-One*. *Open Since*- `%dd%dhr` \n", list[index].SlackChannel, list[index].Severity, days, hours) + } + } + + if text != "" { + msgOption := slack.MsgOptionText(text, false) + _, _, errMessage := socketModeClient.PostMessage(viper.GetString("team.metric.update.channel"), msgOption) + if errMessage != nil { + logger.Error("PostMessage failed for cronJob ", zap.Error(errMessage)) + } + } + } + +} + +func convertSeverityListToMap(severities []severity.SeverityEntity) map[uint]string { + severityMap := make(map[uint]string) + + for _, severity := range severities { + severityMap[severity.ID] = severity.Name + } + + return severityMap +} + +func IncidentMetricBlock(teamName *slack.HeaderBlock, incidentOpenCount *slack.SectionBlock, onCallAndManagerBlock *slack.SectionBlock) slack.Blocks { + return slack.Blocks{ + BlockSet: []slack.Block{ + teamName, + incidentOpenCount, + onCallAndManagerBlock, + }, + } +} + +func builderTeamNameHeader(teamName string) *slack.HeaderBlock { + headerText := slack.NewTextBlockObject(slack.PlainTextType, teamName, true, false) + headerSection := slack.NewHeaderBlock(headerText) + + return headerSection +} + +func buildOpenIncidentText(count int) *slack.SectionBlock { + sectionBlock := slack.NewTextBlockObject(slack.PlainTextType, fmt.Sprintf("Open Incidents: %d", count), true, false) + headerSection := slack.NewSectionBlock(sectionBlock, nil, nil) + + return headerSection +} + +func buildOnCallAndManagerBlock(onCallHandle string, managerHandle string) *slack.SectionBlock { + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*OnCall*\n<@%s>", onCallHandle), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Manager*\n<@%s>", managerHandle), false, false), + } + block := slack.NewSectionBlock(nil, fields, nil) + + return block +} diff --git a/model/incident/incident.go b/model/incident/incident.go index ce1e73a..a0a5e5d 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -380,3 +380,25 @@ func (r *Repository) FindIncidentsByNotResolvedStatusAndGreaterSeverityTatThanCu return incidentEntity, nil } + +func (r *Repository) GetIncidentsByTeamIdAndNotResolved(team_id uint) (*[]IncidentEntity, error) { + var incidentEntity []IncidentEntity + + result := r.gormClient.Order("severity_id").Find(&incidentEntity, "team_id = ? and status <> ?", team_id, 4) + if result.Error != nil { + return nil, result.Error + } + + return &incidentEntity, 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 +} diff --git a/model/incident/model.go b/model/incident/model.go index 4231032..86a8175 100644 --- a/model/incident/model.go +++ b/model/incident/model.go @@ -50,3 +50,11 @@ type AddIncidentStatusRequest struct { Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` } + +type SlackChannelWithResponderId struct { + SlackChannel string `json:"slack_channel,omitempty"` + ResponderId string `json:"responder_id,omitempty"` + CreatedAt time.Time + Severity string + ManagerId string +} diff --git a/model/team/entity.go b/model/team/entity.go index 31c29d0..e357609 100644 --- a/model/team/entity.go +++ b/model/team/entity.go @@ -13,6 +13,7 @@ type TeamEntity struct { OncallHandle string `gorm:"column:oncall_handle"` Active bool `gorm:"column:active"` WebhookSlackChannel string `gorm:"column:webhook_slack_channel"` + ManagerHandle string `gorm:"column:manager_handle"` } func (TeamEntity) TableName() string {