TP-51709 : Enabled Marking an Incident as Duplicate through update Incident API (#336)

* TP-51709| created mark-duplicate-incident-status function

* TP-51709| made the duplicate status code modular
This commit is contained in:
Gullipalli Chetan Kumar
2023-12-22 14:18:43 +05:30
committed by GitHub
parent c9785af64b
commit 74c1b88b3d
10 changed files with 231 additions and 85 deletions

View File

@@ -74,11 +74,11 @@ func BuildSlackTextMessageFromMetaData(metaData []byte, isCodeBlock bool) (slack
return slack.MsgOptionText(textMessage, false), nil
}
// PostArchivingTimeToIncidentChannel posts an archiving time nearest to the next half-hour in incident channel
func PostArchivingTimeToIncidentChannel(channelId string, incidentStatus string, client *socketmode.Client) error {
// GetArchivingTimeToIncidentChannel posts an archiving time nearest to the next half-hour in incident channel
func GetArchivingTimeToIncidentChannel(incidentStatus string) (string, error) {
location, err := time.LoadLocation("Asia/Kolkata")
if err != nil {
return err
return "", err
}
archivalTime := time.Now().Add(time.Duration(viper.GetInt64("cron.job.archival_time_in_hour")) * time.Hour)
archivalTime = archivalTime.In(location)
@@ -89,9 +89,7 @@ func PostArchivingTimeToIncidentChannel(channelId string, incidentStatus string,
roundedArchivalTime = roundedArchivalTime.Add(30 * time.Minute)
}
formattedTime := roundedArchivalTime.Format(fmt.Sprintf("Your incident is `%v` and this channel will be archived at `3:04 PM` on `02 Jan 2006`", incidentStatus))
msgOption := slack.MsgOptionText(fmt.Sprintf(formattedTime), false)
_, _, errMessage := client.PostMessage(channelId, msgOption)
return errMessage
return formattedTime, nil
}
func PostIncidentStatusErrorMessage(status, channelId, userId string, client *socketmode.Client) {

View File

@@ -1,21 +1,22 @@
package action
import (
"errors"
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"houston/appcontext"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/logger"
"houston/model/customErrors"
"houston/model/incident"
"houston/model/severity"
"houston/model/tag"
"houston/model/team"
"houston/service/conference"
incidentService "houston/service/incident"
"strconv"
"time"
)
type DuplicateIncidentAction struct {
@@ -25,6 +26,7 @@ type DuplicateIncidentAction struct {
teamRepository *team.Repository
severityRepository *severity.Repository
calendarService conference.ICalendarService
incidentService incidentService.IIncidentService
}
func NewDuplicateIncidentProcessor(
@@ -33,6 +35,7 @@ func NewDuplicateIncidentProcessor(
tagService *tag.Repository,
teamRepository *team.Repository,
severityRepository *severity.Repository,
incidentService incidentService.IIncidentService,
) *DuplicateIncidentAction {
return &DuplicateIncidentAction{
client: client,
@@ -41,6 +44,7 @@ func NewDuplicateIncidentProcessor(
teamRepository: teamRepository,
severityRepository: severityRepository,
calendarService: appcontext.GetCalendarService(),
incidentService: incidentService,
}
}
@@ -71,82 +75,38 @@ func (dip *DuplicateIncidentAction) DuplicateIncidentRequestProcess(callback sla
}
func (dip *DuplicateIncidentAction) DuplicateIncidentProcess(callback slack.InteractionCallback, request *socketmode.Request) {
channelId := callback.View.PrivateMetadata
user := callback.User
incidentEntity, err := dip.incidentRepository.FindIncidentByChannelId(channelId)
if err != nil {
logger.Error("FindIncidentByChannelId error",
zap.String("incident_slack_channel_id", channelId),
zap.String("user_id", user.ID), zap.Error(err))
logger.Error(fmt.Sprintf("failed to get incident for channel id: %s", channelId))
return
} else if incidentEntity == nil {
logger.Error("IncidentEntity not found ",
zap.String("incident_slack_channel_id", channelId),
zap.String("user_id", user.ID), zap.Error(err))
logger.Error(fmt.Sprintf("IncidentEntity not found for channel id: %s", channelId))
return
}
inputIncidentID := callback.View.State.Values["RCA"]["rca"].Value
duplicateOfId, _ := strconv.Atoi(inputIncidentID)
incidentRca := callback.View.State.Values["RCA"]["rca"].Value
isRcaSet, originalIncident, err := dip.isRcaValid(incidentRca, incidentEntity.ID)
err = dip.incidentService.DuplicateUpdateIncidentStatus(uint(duplicateOfId), user.ID, incidentEntity)
if err != nil {
return
}
if isRcaSet {
now := time.Now()
incidentEntity.Status = 5
incidentEntity.RCA = incidentRca
incidentEntity.UpdatedBy = user.ID
incidentEntity.EndTime = &now
err = dip.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
logger.Error("failed to update incident to duplicate state",
zap.String("channel", channelId),
zap.String("user_id", user.ID), zap.Error(err))
var slackError *customErrors.SlackError
if errors.As(err, &slackError) {
return
}
logger.Info("successfully marked the incident as duplicate",
zap.String("channel", channelId),
zap.String("user_id", user.ID))
msgOption := slack.MsgOptionText(fmt.Sprintf("<@%s> *>* `updated status as %s. Attached to` <#%v>", callback.User.ID,
incident.Duplicated, originalIncident.SlackChannel), false)
_, _, errMessage := dip.client.PostMessage(channelId, msgOption)
if errMessage != nil {
logger.Error("post response failed for duplicate incident process", zap.String("channel", channelId), zap.Error(errMessage))
return
}
go func() {
msgOption = slack.MsgOptionText(fmt.Sprintf("<#%v> `was attached as duplicate to this incident`", channelId), false)
_, _, errMessage = dip.client.PostMessage(originalIncident.SlackChannel, msgOption)
if errMessage != nil {
logger.Error("post response failed for duplicate incident process", zap.String("channel", originalIncident.SlackChannel), zap.Error(errMessage))
}
msgUpdate := NewIncidentChannelMessageUpdateAction(dip.client, dip.incidentRepository, dip.teamRepository, dip.severityRepository)
msgUpdate.ProcessAction(incidentEntity.SlackChannel)
if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id {
postErr := util.PostArchivingTimeToIncidentChannel(channelId, incident.Duplicated, dip.client)
if postErr != nil {
logger.Error("failed to post archiving time to incident channel", zap.String("channel id", channelId), zap.Error(err))
}
}
}()
go func() {
err := dip.calendarService.DeleteEvent(incidentEntity.ConferenceId)
if err != nil {
logger.Error(fmt.Sprintf("Unable to delete conference for incident id: %d", incidentEntity.ID), zap.Error(err))
}
}()
} else {
msgOption := slack.MsgOptionText(fmt.Sprintf("`Submitted incident id: %s is not a valid open incident. Check and resubmit`", incidentRca), false)
msgOption := slack.MsgOptionText(fmt.Sprintf("`Submitted incident id: %s is not a valid open incident. Check and resubmit`", inputIncidentID), false)
_, errMessage := dip.client.PostEphemeral(channelId, user.ID, msgOption)
if errMessage != nil {
logger.Error("post response failed for Mark Duplicate Incident", zap.String("channel", channelId), zap.Error(errMessage))
return
}
}
go func() {
err := dip.calendarService.DeleteEvent(incidentEntity.ConferenceId)
if err != nil {
logger.Error(fmt.Sprintf("Unable to delete conference for incident id: %d", incidentEntity.ID), zap.Error(err))
}
}()
var payload interface{}
dip.client.Ack(*request, payload)
}

View File

@@ -111,9 +111,14 @@ func (irp *ResolveIncidentAction) IncidentResolveProcess(callback slack.Interact
}()
go func() {
if incidentEntity.SeverityId != incident.Sev0Id && incidentEntity.SeverityId != incident.Sev1Id {
postErr := util.PostArchivingTimeToIncidentChannel(channelId, incident.Resolved, irp.client)
if postErr != nil {
logger.Error("failed to post archiving time to incident channel", zap.String("channel id", channelId), zap.Error(err))
msg, err := util.GetArchivingTimeToIncidentChannel(incident.Resolved)
if err != nil {
logger.Error("failed to get archiving time to incident channel", zap.String("channel id", channelId), zap.Error(err))
} else {
_, _, postErr := irp.client.PostMessage(channelId, slack.MsgOptionText(msg, false))
if postErr != nil {
logger.Error("failed to post archiving time to incident channel", zap.String("channel id", channelId), zap.Error(err))
}
}
}
if viper.GetBool("RCA_GENERATION_ENABLED") {

View File

@@ -74,7 +74,7 @@ func NewBlockActionProcessor(
teamService, severityService, slackbotClient),
incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamService, severityService),
tagService, teamService, severityService, incidentServiceV2),
incidentRCASectionAction: action.NewIncidentRCASectionAction(socketModeClient, incidentRepository, teamService, tagService, severityService, action.NewIncidentTagsAction(incidentRepository, tagService), action.NewIncidentRCASummaryAction(incidentRepository), action.NewIncidentJiraLinksAction(incidentServiceV2, slackService), rcaService),
}
}
@@ -232,7 +232,7 @@ func NewViewSubmissionProcessor(
showIncidentSubmitAction: action.NewShowIncidentsSubmitAction(socketModeClient, incidentRepository,
teamRepository),
incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository,
tagService, teamRepository, severityService),
tagService, teamRepository, severityService, incidentServiceV2),
incidentRCAAction: action.NewIncidentRCASectionAction(socketModeClient, incidentRepository, teamService, tagService, severityService, action.NewIncidentTagsAction(incidentRepository, tagService), action.NewIncidentRCASummaryAction(incidentRepository), action.NewIncidentJiraLinksAction(incidentServiceV2, slackService), rcaService),
}
}

View File

@@ -27,3 +27,19 @@ type NotFoundError struct {
func NewNotFoundError(message string) error {
return &NotFoundError{CustomError{Message: message}}
}
type InvalidInputError struct {
CustomError
}
func NewInvalidInputError(message string) error {
return &InvalidInputError{CustomError{Message: message}}
}
type SlackError struct {
CustomError
}
func NewSlackError(message string) error {
return &SlackError{CustomError{Message: message}}
}

View File

@@ -1440,16 +1440,28 @@ func (i *IncidentServiceV2) UpdateStatus(
zap.String("Status", request.Status), zap.Error(err))
return fmt.Errorf("Invalid status ID: %s", request.Status)
}
incidentEntity.Status = uint(num)
if incidentStatus.IsTerminalStatus {
now := time.Now()
incidentEntity.EndTime = &now
}
switch uint(num) {
case incident.DuplicateId:
{
err := i.DuplicateUpdateIncidentStatus(request.DuplicateOfId, userId, incidentEntity)
if err != nil {
return err
}
}
default:
{
incidentEntity.Status = uint(num)
if incidentStatus.IsTerminalStatus {
now := time.Now()
incidentEntity.EndTime = &now
}
err := i.commitIncidentEntity(incidentEntity, userId)
if err != nil {
logger.Error(fmt.Sprintf("%s error in committing update to DB", updateLogTag), zap.Error(err))
return err
err := i.commitIncidentEntity(incidentEntity, userId)
if err != nil {
logger.Error(fmt.Sprintf("%s error in committing update to DB", updateLogTag), zap.Error(err))
return err
}
}
}
err = i.UpdateStatusWorkflow(
@@ -1487,6 +1499,9 @@ func (i *IncidentServiceV2) UpdateStatusWorkflow(
var slackErrors []error
go util.ExecuteConcurrentAction(&waitGroup, func() {
if incidentStatus.ID == incident.DuplicateId {
return
}
_, err := i.slackService.PostMessageByChannelID(
fmt.Sprintf("<@%s> > set status to %s", userId, incidentStatus.Name),
false,

View File

@@ -18,4 +18,5 @@ type IIncidentService interface {
AddJiraLinks(entity *incident.IncidentEntity, jiraLinks ...string) error
RemoveJiraLink(entity *incident.IncidentEntity, jiraLink string) error
IsHoustonChannel(channelID string) (bool, error)
DuplicateUpdateIncidentStatus(duplicateOfID uint, userID string, incidentEntity *incident.IncidentEntity) error
}

View File

@@ -0,0 +1,82 @@
package incident
import (
"fmt"
"go.uber.org/zap"
"houston/common/util"
"houston/logger"
"houston/model/customErrors"
"houston/model/incident"
"strconv"
"sync"
"time"
)
func (i *IncidentServiceV2) DuplicateUpdateIncidentStatus(duplicateOfID uint, userID string, incidentEntity *incident.IncidentEntity) error {
channelID := incidentEntity.SlackChannel
incidentId := incidentEntity.ID
originalIncidentChannel, err := i.validateDuplicateIdAndGetChannelId(duplicateOfID, incidentId)
if err != nil {
return err
}
now := time.Now()
incidentEntity.Status = incident.DuplicateId
incidentEntity.RCA = strconv.Itoa(int(duplicateOfID))
incidentEntity.UpdatedBy = userID
incidentEntity.EndTime = &now
_, postErr := i.slackService.PostMessageByChannelID(fmt.Sprintf("<@%s> *>* `updated status as %s. Attached to` <#%v>",
userID, incident.Duplicated, originalIncidentChannel), false, channelID)
if postErr != nil {
logger.Error(fmt.Sprintf("%s Failed to post message in channel %s", updateLogTag, channelID), zap.Error(postErr))
return customErrors.NewSlackError("Failed to post message in slack channel")
}
err = i.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
logger.Error(fmt.Sprintf("%s Failed to update incident with id %d", updateLogTag, incidentId), zap.Error(err))
return customErrors.NewDataAccessError(fmt.Sprintf("Failed to update incident with id %d", incidentId))
}
i.DuplicateUpdateStatusWorkFlow(channelID, originalIncidentChannel, incidentEntity.SeverityId)
return nil
}
func (i *IncidentServiceV2) validateDuplicateIdAndGetChannelId(duplicateOfID uint, incidentId uint) (string, error) {
if duplicateOfID == incidentId {
return "", customErrors.NewInvalidInputError(fmt.Sprintf("Incident with id: %d cannot be marked as duplicate of itself", incidentId))
}
originalIncident, err := i.incidentRepository.FindIncidentById(duplicateOfID)
if err != nil {
logger.Error(fmt.Sprintf("%s Failed to get incident with id %d", updateLogTag, duplicateOfID), zap.Error(err))
return "", customErrors.NewDataAccessError(fmt.Sprintf("Failed to get incident with id %d", duplicateOfID))
}
if originalIncident.Status == incident.DuplicateId {
return "", customErrors.NewInvalidInputError(fmt.Sprintf("Incident with id: %d is already marked as duplicate", duplicateOfID))
}
return originalIncident.SlackChannel, nil
}
func (i *IncidentServiceV2) DuplicateUpdateStatusWorkFlow(currentChannel, originalIncidentChannel string, severityId uint) {
var waitGroup sync.WaitGroup
waitGroup.Add(1)
go util.ExecuteConcurrentAction(&waitGroup, func() {
if severityId != incident.Sev0Id && severityId != incident.Sev1Id {
msg, err := util.GetArchivingTimeToIncidentChannel(incident.Duplicated)
if err != nil {
logger.Error(fmt.Sprintf("%s Failed to get archiving time for incident channel %s", updateLogTag, currentChannel), zap.Error(err))
return
}
_, postErr := i.slackService.PostMessageByChannelID(msg, false, currentChannel)
if postErr != nil {
logger.Error(fmt.Sprintf("%s Failed to post archiving time to incident channel %s", updateLogTag, currentChannel), zap.Error(postErr))
}
}
})
waitGroup.Add(1)
go util.ExecuteConcurrentAction(&waitGroup, func() {
_, err := i.slackService.PostMessageByChannelID(fmt.Sprintf("<#%v> `was attached as duplicate to this incident`", currentChannel), false, originalIncidentChannel)
if err != nil {
logger.Error(fmt.Sprintf("%s Failed to post message in channel %s", updateLogTag, currentChannel), zap.Error(err))
}
})
waitGroup.Wait()
}

View File

@@ -0,0 +1,68 @@
package incident
import (
"errors"
"houston/model/customErrors"
)
func (suite *IncidentServiceSuite) TestDuplicateUpdateIncidentStatus_InvalidInputError() {
var invalidInputError *customErrors.InvalidInputError
mockCurrentIncident := GetMockIncident()
mockOriginalIncident := GetMockIncident()
err1 := suite.incidentService.DuplicateUpdateIncidentStatus(mockCurrentIncident.ID, "userID", mockCurrentIncident)
suite.Error(err1)
suite.ErrorAs(err1, &invalidInputError)
mockOriginalIncident.ID = 2
mockOriginalIncident.Status = 5
suite.incidentRepository.FindIncidentByIdMock.When(mockOriginalIncident.ID).Then(mockOriginalIncident, nil)
err2 := suite.incidentService.DuplicateUpdateIncidentStatus(mockOriginalIncident.ID, "userID", mockCurrentIncident)
suite.Error(err2)
suite.ErrorAs(err2, &invalidInputError)
}
func (suite *IncidentServiceSuite) TestDuplicateUpdateIncidentStatus_DBError() {
var dataAccessError *customErrors.DataAccessError
mockCurrentIncident := GetMockIncident()
mockOriginalIncident := GetMockIncident()
mockOriginalIncident.ID = 2
suite.incidentRepository.FindIncidentByIdMock.When(mockOriginalIncident.ID).Then(nil, errors.New("DBError"))
err1 := suite.incidentService.DuplicateUpdateIncidentStatus(mockOriginalIncident.ID, "userID", mockCurrentIncident)
suite.Error(err1)
suite.ErrorAs(err1, &dataAccessError)
mockOriginalIncident.ID = 3
suite.incidentRepository.FindIncidentByIdMock.When(mockOriginalIncident.ID).Then(mockOriginalIncident, nil)
suite.slackService.PostMessageByChannelIDMock.Return("", nil)
suite.incidentRepository.UpdateIncidentMock.When(mockCurrentIncident).Then(errors.New("DBError"))
err2 := suite.incidentService.DuplicateUpdateIncidentStatus(mockOriginalIncident.ID, "userID", mockCurrentIncident)
suite.Error(err2)
suite.ErrorAs(err2, &dataAccessError)
}
func (suite *IncidentServiceSuite) TestDuplicateUpdateIncidentStatus_SlackError() {
var slackError *customErrors.SlackError
mockCurrentIncident := GetMockIncident()
mockOriginalIncident := GetMockIncident()
mockOriginalIncident.ID = 2
suite.incidentRepository.FindIncidentByIdMock.When(mockOriginalIncident.ID).Then(mockOriginalIncident, nil)
suite.slackService.PostMessageByChannelIDMock.Return("", errors.New("SlackError"))
err := suite.incidentService.DuplicateUpdateIncidentStatus(mockOriginalIncident.ID, "userID", mockCurrentIncident)
suite.Error(err)
suite.ErrorAs(err, &slackError)
}
func (suite *IncidentServiceSuite) TestDuplicateUpdateIncidentStatus_Success() {
mockCurrentIncident := GetMockIncident()
mockOriginalIncident := GetMockIncident()
mockOriginalIncident.ID = 2
suite.incidentRepository.FindIncidentByIdMock.When(mockOriginalIncident.ID).Then(mockOriginalIncident, nil)
suite.incidentRepository.UpdateIncidentMock.When(mockCurrentIncident).Then(nil)
suite.slackService.PostMessageByChannelIDMock.Return("", nil)
err := suite.incidentService.DuplicateUpdateIncidentStatus(mockOriginalIncident.ID, "userID", mockCurrentIncident)
suite.NoError(err)
}

View File

@@ -1,9 +1,10 @@
package service
type UpdateIncidentRequest struct {
Id uint `json:"id"`
Status string `json:"status,omitempty"`
TeamId string `json:"teamId,omitempty"`
SeverityId string `json:"severityId,omitempty"`
MetaData CreateIncidentMetaData `json:"metaData,omitempty"`
Id uint `json:"id"`
Status string `json:"status,omitempty"`
TeamId string `json:"teamId,omitempty"`
SeverityId string `json:"severityId,omitempty"`
MetaData CreateIncidentMetaData `json:"metaData,omitempty"`
DuplicateOfId uint `json:"duplicateOfId,omitempty"`
}