Files
houston-be/service/orchestration/incident_orchestrator_impl.go

501 lines
15 KiB
Go

package orchestration
import (
"encoding/json"
"fmt"
slack2 "github.com/slack-go/slack"
"go.uber.org/zap"
"gorm.io/gorm"
"houston/appcontext"
"houston/common/util"
"houston/logger"
"houston/model/incident"
"houston/model/product"
"houston/model/team"
user2 "houston/model/user"
incidentService "houston/service/incident"
incidentServiceImpl "houston/service/incident/impl"
"houston/service/products"
"houston/service/productsTeams"
incidentRequest "houston/service/request/incident"
response "houston/service/response"
"houston/service/severity"
"houston/service/slack"
"houston/service/teamService"
"houston/service/teamUser"
"houston/service/user"
utils "houston/service/utils"
"sort"
"sync"
"time"
)
type incidentOrchestratorImpl struct {
productTeamService productsTeams.ProductTeamsService
productsService products.ProductService
teamUserService teamUser.ITeamUserService
userService user.UserService
teamService teamService.ITeamServiceV2
severityService severity.ISeverityService
slackService slack.ISlackService
incidentService incidentService.IIncidentService
}
func newIncidentOrchestratorImpl(
productTeamService productsTeams.ProductTeamsService,
productsService products.ProductService,
teamUserService teamUser.ITeamUserService,
userService user.UserService,
teamService teamService.ITeamServiceV2,
severityService severity.ISeverityService,
slackService slack.ISlackService,
incidentService incidentService.IIncidentService,
) *incidentOrchestratorImpl {
return &incidentOrchestratorImpl{
productTeamService: productTeamService,
productsService: productsService,
teamUserService: teamUserService,
userService: userService,
teamService: teamService,
severityService: severityService,
slackService: slackService,
incidentService: incidentService,
}
}
const (
defaultTeam = "Others"
logTag = "[incident_orchestrator]"
)
func (i *incidentOrchestratorImpl) GetProductsOfUserByEmailID(emailID string) (*response.ProductsOfUser, error) {
return i.getProductsOfTeams(i.getTeamsOfUserByEmailID(emailID))
}
func (i *incidentOrchestratorImpl) GetProductsOfUserBySlackUserID(slackUserID string) (*response.ProductsOfUser, error) {
return i.getProductsOfTeams(i.getTeamsOfUserBySlackUserID(slackUserID))
}
func (i *incidentOrchestratorImpl) getProductsOfTeams(teams []team.TeamDTO) (*response.ProductsOfUser, error) {
var productsOfUser []product.ProductDTO
var err error
var teamIDs []uint
for _, t := range teams {
teamIDs = append(teamIDs, t.ID)
}
productsOfUser = i.productTeamService.GetProductsByTeamId(teamIDs...)
allProducts, err := i.productsService.GetAllProducts()
if err != nil {
return nil, err
}
var defaultProduct *product.ProductDTO = nil
if len(productsOfUser) == 1 {
defaultProduct = &productsOfUser[0]
}
return &response.ProductsOfUser{
DefaultProduct: defaultProduct,
Products: allProducts,
}, nil
}
func (i *incidentOrchestratorImpl) GetReportingAndResponderTeams(
emailID string, productIDs []uint,
) (*response.ReportingAndResponderTeams, error) {
reportingTeams := i.getTeamsOfUserByEmailID(emailID)
return i.getReportingAndResponderTeams(reportingTeams, productIDs)
}
func (i *incidentOrchestratorImpl) GetReportingAndResponderTeamsBySlackUserId(
slackUserID string, productIDs []uint,
) (*response.ReportingAndResponderTeams, error) {
reportingTeams := i.getTeamsOfUserBySlackUserID(slackUserID)
return i.getReportingAndResponderTeams(reportingTeams, productIDs)
}
func (i *incidentOrchestratorImpl) GetResponderTeamsForUpdate(
slackChannelID string, productIDs []uint,
) *response.ProductAndUserTeams {
logger.Info("[GetResponderTeamsForUpdate] channel id is: " + slackChannelID)
incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID)
if err != nil {
logger.Error(fmt.Sprintf("%s Error in fetching incident by channel id: %s", logTag, slackChannelID), zap.Error(err))
return nil
}
teamEntity, err := i.teamService.GetTeamById(incidentEntity.TeamId)
if err != nil {
logger.Error(fmt.Sprintf("%s Error in fetching default team", logTag), zap.Error(err))
}
if productIDs == nil {
currentProducts := incidentEntity.Products
for _, p := range currentProducts {
productIDs = append(productIDs, p.ID)
}
}
teamsOfProducts, err := i.getTeamsOfProducts(productIDs)
if err != nil {
return nil
}
if ifTeamExists(teamsOfProducts, teamEntity) {
teamsOfProducts.DefaultTeam = teamEntity.ToDTO()
}
return teamsOfProducts
}
func (i *incidentOrchestratorImpl) GetProductsForUpdate(slackChannelID string) *response.ProductsForUpdate {
incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID)
if err != nil {
return nil
}
var currentProducts []product.ProductDTO
for _, p := range incidentEntity.Products {
currentProducts = append(currentProducts, *p.ToDTO())
}
allProducts, err := i.productsService.GetAllProducts()
if err != nil {
return nil
}
productsForUpdate := &response.ProductsForUpdate{
DefaultProducts: currentProducts,
Products: allProducts,
}
return productsForUpdate
}
func ifTeamExists(teamsOfProducts *response.ProductAndUserTeams, teamEntity *team.TeamEntity) bool {
for _, t := range teamsOfProducts.Teams {
if t.ID == teamEntity.ID {
return true
}
}
return false
}
func (i *incidentOrchestratorImpl) getReportingAndResponderTeams(
teamsOfUser []team.TeamDTO, productIDs []uint,
) (*response.ReportingAndResponderTeams, error) {
if len(teamsOfUser) == 0 {
defaultTeam, err := i.getDefaultTeam()
if err != nil {
return nil, fmt.Errorf("error in fetching default team")
}
teamsOfUser = append(teamsOfUser, *defaultTeam)
}
teamsOfProducts, err := i.getTeamsOfProducts(productIDs)
if err != nil {
return nil, err
}
var defaultSelectedReportingTeam *team.TeamDTO = nil
sort.Sort(sortListOfTeamDTOByName(teamsOfUser))
sort.Sort(sortListOfTeamDTOByName(teamsOfProducts.Teams))
defaultSelectedReportingTeam = &teamsOfUser[0]
var reportingTeams []team.TeamDTO
reportingTeams = append(reportingTeams, teamsOfUser...)
reportingTeams = append(reportingTeams, teamsOfProducts.Teams...)
reportingTeams = util.RemoveDuplicates(reportingTeams, "ID").([]team.TeamDTO)
return &response.ReportingAndResponderTeams{
ReportingTeam: &response.ProductAndUserTeams{DefaultTeam: defaultSelectedReportingTeam, Teams: reportingTeams},
ResponderTeam: teamsOfProducts,
}, nil
}
func (i *incidentOrchestratorImpl) CreateIncident(
request *incidentRequest.CreateIncidentRequestV3, blazeGroupChannelID string,
) (*response.IncidentResponse, error) {
tx := appcontext.GetDB().Begin()
defer util.RollbackTransaction(tx)
incidentEntity, channelTopic, err := i.createIncidentEntityTransactionally(request, tx)
if err != nil {
tx.Rollback()
return nil, err
}
slackChannel, err := i.createSlackChannelTransactionally(
incidentEntity.ID,
incidentEntity.IncidentName,
utils.ConstructIncidentChannelName(
incidentEntity.ID, request.SeverityID, string(incident.Investigating), incidentEntity.Title,
),
request.IsPrivate,
)
if err != nil {
tx.Rollback()
return nil, err
}
tx.Commit()
err = i.updateSlackDetailsIntoIncidentEntity(slackChannel, incidentEntity)
if err != nil {
return nil, err
}
err = i.setChannelTopic(slackChannel, channelTopic)
if err != nil {
return nil, err
}
go func() {
i.incidentService.PostIncidentCreationWorkflow(
slackChannel,
incidentEntity.ID,
blazeGroupChannelID,
)
}()
i.incidentService.EeMonitoringServiceWebhookCall(incidentEntity)
incidentResponse := response.ConvertToIncidentResponse(*incidentEntity)
return &incidentResponse, nil
}
func (i *incidentOrchestratorImpl) getTeamsOfUserByEmailID(emailID string) []team.TeamDTO {
userDTO, err := i.userService.GetHoustonUserByEmailId(emailID)
if err != nil {
logger.Error(fmt.Sprintf("%s Error in fetching user by email id", logTag), zap.Error(err))
}
return i.getTeamByUserDTO(userDTO)
}
func (i *incidentOrchestratorImpl) getTeamsOfUserBySlackUserID(slackUserId string) []team.TeamDTO {
userDTO, err := i.userService.GetHoustonUserBySlackUserId(slackUserId)
if err != nil {
return nil
}
return i.getTeamByUserDTO(userDTO)
}
func (i *incidentOrchestratorImpl) getTeamByUserDTO(userDTO *user2.UserDTO) []team.TeamDTO {
if userDTO == nil {
return nil
}
teamUserDTO, err := i.teamUserService.GetTeamsByUserId(userDTO.ID)
if err != nil {
return nil
}
var teams []team.TeamDTO
if len(teamUserDTO) > 0 {
for _, t := range teamUserDTO {
teams = append(teams, t.Team)
}
}
return teams
}
func (i *incidentOrchestratorImpl) getTeamsOfProducts(productIDs []uint) (*response.ProductAndUserTeams, error) {
productTeamDTO, err := i.productTeamService.GetProductTeamsByProductIDs(productIDs)
if err != nil {
logger.Error(fmt.Sprintf("%s Error in fetching product teams", logTag), zap.Error(err))
}
if len(productTeamDTO) == 0 {
return i.defaultProductAndUserTeams(), nil
}
var allTeams []team.TeamDTO
for _, pt := range productTeamDTO {
allTeams = append(allTeams, pt.Teams...)
}
if len(allTeams) == 0 {
return i.defaultProductAndUserTeams(), nil
}
allUniqueTeams := util.RemoveDuplicates(allTeams, "ID").([]team.TeamDTO)
var defaultSelectedTeam *team.TeamDTO = nil
if len(allUniqueTeams) == 1 {
defaultSelectedTeam = &allUniqueTeams[0]
}
return &response.ProductAndUserTeams{
DefaultTeam: defaultSelectedTeam,
Teams: allUniqueTeams,
}, nil
}
func (i *incidentOrchestratorImpl) defaultProductAndUserTeams() *response.ProductAndUserTeams {
defaultTeam, _ := i.getDefaultTeam()
return &response.ProductAndUserTeams{
DefaultTeam: defaultTeam,
Teams: []team.TeamDTO{*defaultTeam},
}
}
func (i *incidentOrchestratorImpl) getDefaultTeam() (*team.TeamDTO, error) {
tr, err := i.teamService.GetTeamByName(defaultTeam)
if err != nil {
return nil, fmt.Errorf("error in fetching default team")
}
return tr.ToDTO(), nil
}
func (i *incidentOrchestratorImpl) buildCreateIncidentDTO(
createIncRequest *incidentRequest.CreateIncidentRequestV3,
) (*incident.CreateIncidentDTO, *incidentServiceImpl.ChannelTopic, error) {
var (
createIncidentDTO incident.CreateIncidentDTO
err error
wg sync.WaitGroup
mutex sync.Mutex
channelTopic = &incidentServiceImpl.ChannelTopic{}
)
wg.Add(5)
go func() {
defer wg.Done()
reportingTeam, err := i.teamService.GetTeamDetails(createIncRequest.ReportingTeamID)
if err != nil {
return
}
mutex.Lock()
createIncidentDTO.ReportingTeamID = &reportingTeam.ID
channelTopic.ReportingTeamName = reportingTeam.Name
mutex.Unlock()
}()
go func() {
defer wg.Done()
responderTeam, err := i.teamService.GetTeamDetails(createIncRequest.ResponderTeamID)
if err != nil {
return
}
mutex.Lock()
createIncidentDTO.TeamId = fmt.Sprintf("%d", responderTeam.ID)
channelTopic.ResponderTeamName = responderTeam.Name
mutex.Unlock()
}()
go func() {
defer wg.Done()
severityDTO, err := i.severityService.FindSeverityById(createIncRequest.SeverityID)
if err != nil {
return
}
mutex.Lock()
createIncidentDTO.Severity = fmt.Sprintf("%d", severityDTO.ID)
channelTopic.SeverityName = severityDTO.Name
mutex.Unlock()
}()
go func() {
defer wg.Done()
productDTOs, err := i.productsService.GetProductsByIDs(createIncRequest.ProductIds)
if err != nil {
return
}
if len(productDTOs) != len(createIncRequest.ProductIds) {
err = fmt.Errorf("error in fetching product details")
return
}
var productNames []string
for _, product := range productDTOs {
productNames = append(productNames, product.ProductName)
}
mutex.Lock()
createIncidentDTO.ProductIds = createIncRequest.ProductIds
channelTopic.ProductNames = productNames
mutex.Unlock()
}()
go func() {
defer wg.Done()
mutex.Lock()
createIncidentDTO.CreatedBy = i.slackService.GetSlackUserIdOrEmail(createIncRequest.CreatedBy)
mutex.Unlock()
}()
wg.Wait()
if err != nil {
return nil, nil, err
}
createIncidentDTO.Title = createIncRequest.Title
channelTopic.Title = createIncRequest.Title
createIncidentDTO.Description = createIncRequest.Description
createIncidentDTO.Status = incident.Investigating
channelTopic.StatusName = string(incident.Investigating)
createIncidentDTO.UpdatedBy = createIncidentDTO.CreatedBy
createIncidentDTO.StartTime = time.Now()
createIncidentDTO.EnableReminder = false
createIncidentDTO.IsPrivate = createIncRequest.IsPrivate
if createIncRequest.Metadata != nil {
metadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*createIncRequest.Metadata})
createIncidentDTO.MetaData = metadata
}
return &createIncidentDTO, channelTopic, nil
}
func (i *incidentOrchestratorImpl) createIncidentEntityTransactionally(
request *incidentRequest.CreateIncidentRequestV3, tx *gorm.DB,
) (*incident.IncidentEntity, *incidentServiceImpl.ChannelTopic, error) {
createIncidentDTO, channelTopic, err := i.buildCreateIncidentDTO(request)
if err != nil {
return nil, nil, err
}
entity, err := i.incidentService.CreateIncidentEntity(createIncidentDTO, tx)
if err != nil {
logger.Error(fmt.Sprintf("%s Error while creating incident", logTag), zap.Error(err))
return nil, nil, err
}
logger.Info(fmt.Sprintf("%s Incident entity created. Incident is: %s", logTag, entity.IncidentName))
return entity, channelTopic, nil
}
func (i *incidentOrchestratorImpl) createSlackChannelTransactionally(incidentID uint, incidentName string, channelName string, isPrivate bool) (*slack2.Channel, error) {
channel, err := i.slackService.CreateSlackChannel(incidentID, channelName, isPrivate)
if err != nil {
logger.Error(
fmt.Sprintf("%s [%s] Error while crating slack channel", logTag, incidentName),
zap.Error(err),
)
return nil, err
}
logger.Info(fmt.Sprintf(
"%s [%s] Slack channel created. Channel name is %s", logTag, incidentName, channel.Name),
)
return channel, nil
}
func (i *incidentOrchestratorImpl) setChannelTopic(
channel *slack2.Channel, channelTopic *incidentServiceImpl.ChannelTopic,
) error {
err := i.slackService.SetTopicOfConversation(channel, channelTopic.String())
if err != nil {
logger.Error(
fmt.Sprintf("%s [%s] Failed to set channel topic", logTag, channel.Name),
zap.Error(err),
)
return err
}
logger.Info(fmt.Sprintf("%s [%s] Channel topic is set", logTag, channel.Name))
return nil
}
func (i *incidentOrchestratorImpl) updateSlackDetailsIntoIncidentEntity(channel *slack2.Channel, incident *incident.IncidentEntity) error {
incident.SlackChannel = channel.ID
incident.IncidentName = channel.Name
incident.UpdatedBy = incident.CreatedBy
err := i.incidentService.UpdateIncidentEntity(incident)
if err != nil {
logger.Error(fmt.Sprintf("%s [%s] Failed to update the slack channel details in DB", logTag, incident.IncidentName))
return err
}
logger.Info(fmt.Sprintf("%s [%s] Slack channel details updated to incident entity", logTag, incident.IncidentName))
return nil
}
type sortListOfTeamDTOByName []team.TeamDTO
func (t sortListOfTeamDTOByName) Len() int {
return len(t)
}
func (t sortListOfTeamDTOByName) Less(i, j int) bool {
return t[i].Name < t[j].Name
}
func (t sortListOfTeamDTOByName) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}