Release 1 (#209)

* TP-43474 | Fixing time difference calculation issue (#193)

* TP-43298 | Adding SLA messagae to incident channel below incident summary (#183)

* TP-43298 | Resolving merge conflicts

* TP-43298 | Updating serverity check condition

* TP-43625 | Fixing user id check (#201)

* TP-43625 | Fixing user id check

* TP-43625 | Fixing user id check

* Incident service v2 (#191)

* TP-43339 | create-incident-v2

* TP-43339 | create-incident-v2 handler

* TP-43339 | added logs and some optimiations

* TP-43339 | added slack processor v2

* TP-43339 | create-incident-v2 integration with slack

* TP-43339 | adding feature flag for create-incident-v2

* TP-43339 | removed redundant entity fetch

* TP-43339 | Added SLA related changes to create-incident-v2

* TP-43339 | Posting incident summary to blazeGroup channel and improved logs

* TP-43339 | Moving post summary method to the incident service file

* TP-43339 | Removed socketModeClient usage from incident-service-v2, moved it to slack-service

* TP-43339 | Updated env variable for create-incident-v2

---------

Co-authored-by: Sriram Bhargav <sriram.bhargav@navi.com>
This commit is contained in:
Shashank Shekhar
2023-10-09 16:04:04 +05:30
committed by GitHub
parent 17e2688c1c
commit 1f97532f50
21 changed files with 1173 additions and 37 deletions

View File

@@ -0,0 +1,53 @@
package handler
import (
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
"houston/service/incident"
request "houston/service/request"
common "houston/service/response/common"
utils "houston/service/utils"
"net/http"
)
const (
logTag = "[incident_handler]"
)
type IncidentHandler struct {
gin *gin.Engine
logger *zap.Logger
db *gorm.DB
}
func NewIncidentHandler(gin *gin.Engine, logger *zap.Logger, db *gorm.DB) *IncidentHandler {
return &IncidentHandler{
gin: gin,
logger: logger,
db: db,
}
}
func (h *IncidentHandler) HandleCreateIncident(c *gin.Context) {
var createIncidentRequest request.CreateIncidentRequestV2
if err := c.ShouldBindJSON(&createIncidentRequest); err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}
if err := utils.ValidateCreateIncidentRequestV2(createIncidentRequest); err != nil {
h.logger.Error(fmt.Sprintf("%s Received invalid request to create new incident", logTag), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
incidentServiceV2 := incident.NewIncidentServiceV2(h.logger, h.db)
incidentResponse, err := incidentServiceV2.CreateIncident(createIncidentRequest, "API", "")
if err != nil {
h.logger.Error(fmt.Sprintf("%s Failed to create incident", logTag), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse(incidentResponse, http.StatusOK))
}

View File

@@ -59,7 +59,7 @@ func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient *
logger, socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient,
),
viewSubmissionProcessor: processor.NewViewSubmissionProcessor(
logger, socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient,
logger, socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient,
),
slashCommandResolver: resolver.NewSlashCommandResolver(
logger, diagnosticCommandProcessor, slashCommandProcessor,

View File

@@ -36,6 +36,7 @@ func NewServer(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, mjolnirClient *
func (s *Server) Handler(houstonGroup *gin.RouterGroup) {
s.readinessHandler(houstonGroup)
s.incidentClientHandler(houstonGroup)
s.incidentClientHandlerV2(houstonGroup)
s.filtersHandlerV2(houstonGroup)
s.gin.Use(s.createMiddleware())
s.teamHandler(houstonGroup)
@@ -110,6 +111,18 @@ func (s *Server) incidentClientHandler(houstonGroup *gin.RouterGroup) {
houstonGroup.POST("/create-incident", incidentHandler.CreateIncident)
}
func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) {
houstonGroup.Use(func(c *gin.Context) {
// Add your desired header key-value pair
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
c.Writer.Header().Set("Access-Control-Allow-Headers", viper.GetString("allowed.custom.headers"))
origin := c.Request.Header.Get("Origin")
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
})
incidentHandler := handler.NewIncidentHandler(s.gin, s.logger, s.db)
houstonGroup.POST("/create-incident-v2", incidentHandler.HandleCreateIncident)
}
func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) {
houstonClient := NewHoustonClient(s.logger)
incidentHandler := service.NewIncidentService(s.gin, s.logger, s.db, houstonClient.socketModeClient)

View File

@@ -34,9 +34,14 @@ func main() {
r.Use(ginzap.RecoveryWithZap(logger, true))
houston := r.Group("/houston")
db := postgres.NewGormClient(viper.GetString("postgres.dsn"), viper.GetString("postgres.connection.max.idle.time"),
viper.GetString("postgres.connection.max.lifetime"), viper.GetInt("postgres.connections.max.idle"),
viper.GetInt("postgres.connections.max.open"), logger)
db := postgres.NewGormClient(
viper.GetString("postgres.dsn"),
viper.GetString("postgres.connection.max.idle.time"),
viper.GetString("postgres.connection.max.lifetime"),
viper.GetInt("postgres.connections.max.idle"),
viper.GetInt("postgres.connections.max.open"),
logger,
)
httpClient := clients.NewHttpClient()
mjolnirClient := clients.NewMjolnirClient(httpClient.HttpClient, logger,
viper.GetString("mjolnir.service.url"), viper.GetString("mjolnir.realm.id"),

View File

@@ -5,7 +5,13 @@ import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"go.uber.org/zap"
"houston/model/incident"
"houston/model/severity"
"houston/model/team"
request "houston/service/request"
"math"
"time"
)
func GetColorBySeverity(severityId uint) string {
@@ -79,3 +85,45 @@ func RemoveDuplicateStr(strSlice []string) []string {
}
return list
}
func PostIncidentSeverityEscalationHeadsUpMessage(severities *[]severity.SeverityEntity, incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, client *socketmode.Client) {
fromSeverityName := getSeverityById(severities, incidentEntity.SeverityId)
toSeverityName := getSeverityById(severities, incidentEntity.SeverityId-1)
daysToEscalation := calculateDifferenceInDays(incidentEntity.SeverityTat, time.Now())
teamSlackChannel := teamEntity.WebhookSlackChannel
if daysToEscalation != 0 {
msgOption := slack.MsgOptionText(fmt.Sprintf("This incident will be auto-escalated to `%v` in `%v day(s)`", toSeverityName, daysToEscalation), false)
_, _, err := client.PostMessage(incidentEntity.SlackChannel, msgOption)
if err != nil {
logger.Info(fmt.Sprintf("Error posting message to incident channel for incident %v", incidentEntity.IncidentName), zap.Error(err))
return
}
if teamSlackChannel != "" {
msgOption := slack.MsgOptionText(fmt.Sprintf("<#%s> (`%v`) will be auto-escalated to `%v` in `%v day(s)`", incidentEntity.SlackChannel, fromSeverityName, toSeverityName, daysToEscalation), false)
_, _, err := client.PostMessage(teamSlackChannel, msgOption)
if err != nil {
logger.Info(fmt.Sprintf("Error posting message to team channel for incident %v", incidentEntity.IncidentName), zap.Error(err))
return
}
}
}
}
func getSeverityById(severities *[]severity.SeverityEntity, severityId uint) string {
for _, severityEntity := range *severities {
if severityEntity.ID == severityId {
return severityEntity.Name
}
}
return ""
}
func calculateDifferenceInDays(fromTime, toTime time.Time) int {
fromDate := time.Date(fromTime.Year(), fromTime.Month(), fromTime.Day(), 0, 0, 0, 0, time.UTC)
toDate := time.Date(toTime.Year(), toTime.Month(), toTime.Day(), 0, 0, 0, 0, time.UTC)
return int(math.Abs(toDate.Sub(fromDate).Hours() / 24))
}
func PostMessageToIncidentChannel(message string, channelId string, client *socketmode.Client) error {
msgOption := slack.MsgOptionText(message, false)
_, _, errMessage := client.PostMessage(channelId, msgOption)
return errMessage
}

View File

@@ -0,0 +1,11 @@
package util
import "time"
func GetCurrentTime() int64 {
timeInIst, err := time.LoadLocation("Asia/Kolkata")
if err != nil {
return 0
}
return time.Now().In(timeInIst).UnixNano() / 1e6
}

18
common/util/json_util.go Normal file
View File

@@ -0,0 +1,18 @@
package util
import (
"encoding/json"
)
func JsonToStruct[JsonObject any, Struct any](obj JsonObject, result *Struct) error {
messageBytes, err := json.Marshal(obj)
if err != nil {
return err
}
err = json.Unmarshal(messageBytes, &result)
if err != nil {
return err
}
return nil
}

View File

@@ -69,4 +69,6 @@ grafana.token=GRAFANA_TOKEN
config.sa.keys=CONFIG_SA_KEYS
create-incident.description.max-length=500
create-incident.title.max-length=100
create-incident.title.max-length=100
create-incident-v2-enabled=CREATE_INCIDENT_V2_ENABLED

3
go.mod
View File

@@ -18,7 +18,6 @@ require (
github.com/spf13/viper v1.16.0
github.com/thoas/go-funk v0.9.3
go.uber.org/zap v1.24.0
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.2
gorm.io/gorm v1.25.2
)
@@ -46,7 +45,6 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
@@ -66,7 +64,6 @@ require (
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.55.0 // indirect
gorm.io/driver/mysql v1.4.7 // indirect
)
require (

View File

@@ -22,8 +22,13 @@ type IncidentChannelMessageUpdateAction struct {
severityService *severity.Repository
}
func NewIncidentChannelMessageUpdateAction(socketModeClient *socketmode.Client, logger *zap.Logger,
incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository) *IncidentChannelMessageUpdateAction {
func NewIncidentChannelMessageUpdateAction(
socketModeClient *socketmode.Client,
logger *zap.Logger,
incidentService *incident.Repository,
teamService *team.Repository,
severityService *severity.Repository,
) *IncidentChannelMessageUpdateAction {
return &IncidentChannelMessageUpdateAction{
socketModeClient: socketModeClient,
logger: logger,

View File

@@ -4,12 +4,15 @@ import (
"context"
"encoding/json"
"fmt"
"gorm.io/gorm"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/model/incident"
"houston/model/severity"
"houston/model/team"
"houston/pkg/slackbot"
incidentService "houston/service/incident"
request "houston/service/request"
"strings"
"time"
@@ -30,10 +33,18 @@ type CreateIncidentAction struct {
slackbotClient *slackbot.Client
teamRepository *team.Repository
severityRepository *severity.Repository
db *gorm.DB
}
func NewCreateIncidentProcessor(client *socketmode.Client, logger *zap.Logger, incidentService *incident.Repository,
teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client) *CreateIncidentAction {
func NewCreateIncidentProcessor(
client *socketmode.Client,
logger *zap.Logger,
incidentService *incident.Repository,
teamService *team.Repository,
severityService *severity.Repository,
slackbotClient *slackbot.Client,
db *gorm.DB,
) *CreateIncidentAction {
return &CreateIncidentAction{
client: client,
logger: logger,
@@ -41,16 +52,17 @@ func NewCreateIncidentProcessor(client *socketmode.Client, logger *zap.Logger, i
teamRepository: teamService,
severityRepository: severityService,
slackbotClient: slackbotClient,
db: db,
}
}
func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback slack.InteractionCallback, request *socketmode.Request) {
// Build create incident request
createIncidentRequest := buildCreateIncidentRequest(callback)
createIncidentRequest := buildCreateIncidentDTO(callback)
isp.logger.Info("[CIP] incident request created", zap.Any("request", createIncidentRequest))
// Save the incident to the database
incidentEntity, err := isp.incidentRepository.CreateIncident(createIncidentRequest)
incidentEntity, err := isp.incidentRepository.CreateIncidentEntity(createIncidentRequest)
if err != nil {
isp.logger.Error("[CIP] Error while creating incident", zap.Error(err))
return
@@ -104,6 +116,15 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback s
isp.logger.Error("[CIP] error while assigning responder to the incident ", zap.Error(err))
}
if incidentEntity.SeverityId >= 3 && incidentEntity.Status <= 4 {
slaDate := time.Now().AddDate(0, 0, severityEntity.Sla).Format("02 Jan 2006")
message := fmt.Sprintf("SLA for this incident is `%v day(s)` and will be closed by `%s`", severityEntity.Sla,
slaDate)
err := util.PostMessageToIncidentChannel(message, incidentEntity.SlackChannel, isp.client)
if err != nil {
return
}
}
if viper.GetBool("ENABLE_GMEET") {
gmeet, err := createGmeetLink(isp.logger, *channelID)
if err != nil {
@@ -136,6 +157,30 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessing(callback s
}()
}
func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessingV2(
callback slack.InteractionCallback,
request *socketmode.Request,
) {
// Build create incident request
createIncidentRequest, err := buildCreateIncidentRequestV2(callback)
if err != nil {
isp.logger.Error("[CIP] Error in building CreateIncidentRequestV2", zap.Error(err))
return
}
isp.logger.Info("[CIP] incident request created", zap.Any("request", createIncidentRequest))
service := incidentService.NewIncidentServiceV2(isp.logger, isp.db)
_, err = service.CreateIncident(*createIncidentRequest, "SLACK", callback.View.PrivateMetadata)
if err != nil {
isp.logger.Error("[CIP] Error while creating incident", zap.Error(err))
return
}
// Acknowledge the interaction callback
var payload interface{}
isp.client.Ack(*request, payload)
}
func createGmeetLink(logger *zap.Logger, channelName string) (string, error) {
calclient, err := calendar.NewService(context.Background(), option.WithCredentialsFile(viper.GetString("GMEET_CONFIG_FILE_PATH")))
if err != nil {
@@ -220,11 +265,14 @@ func (isp *CreateIncidentAction) postIncidentSummary(blazeGroupChannelID, incide
_, timestamp, err := isp.client.PostMessage(blazeGroupChannelID, slack.MsgOptionAttachments(att))
if err == nil {
isp.incidentRepository.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{
err := isp.incidentRepository.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{
SlackChannel: blazeGroupChannelID,
MessageTimeStamp: timestamp,
IncidentId: incidentEntity.ID,
})
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("[CIP] error in saving message %v", err)
}
@@ -262,9 +310,9 @@ func (isp *CreateIncidentAction) getTeamAndSeverityAndStatus(teamId, severityId,
return teamEntity, severityEntity, incidentStatusEntity, nil
}
func buildCreateIncidentRequest(callback slack.InteractionCallback) *incident.CreateIncidentRequest {
func buildCreateIncidentDTO(callback slack.InteractionCallback) *incident.CreateIncidentDTO {
blockActions := callback.View.State.Values
var createIncidentRequest incident.CreateIncidentRequest
var createIncidentDTO incident.CreateIncidentDTO
var requestMap = make(map[string]string, 0)
for _, actions := range blockActions {
@@ -279,13 +327,39 @@ func buildCreateIncidentRequest(callback slack.InteractionCallback) *incident.Cr
}
desRequestMap, _ := json.Marshal(requestMap)
json.Unmarshal(desRequestMap, &createIncidentRequest)
json.Unmarshal(desRequestMap, &createIncidentDTO)
createIncidentRequest.Status = incident.Investigating
createIncidentRequest.StartTime = time.Now()
createIncidentRequest.EnableReminder = false
createIncidentRequest.CreatedBy = callback.User.ID
createIncidentRequest.UpdatedBy = callback.User.ID
createIncidentDTO.Status = incident.Investigating
createIncidentDTO.StartTime = time.Now()
createIncidentDTO.EnableReminder = false
createIncidentDTO.CreatedBy = callback.User.ID
createIncidentDTO.UpdatedBy = callback.User.ID
return &createIncidentRequest
return &createIncidentDTO
}
func buildCreateIncidentRequestV2(callback slack.InteractionCallback) (*request.CreateIncidentRequestV2, error) {
blockActions := callback.View.State.Values
var createIncidentRequest request.CreateIncidentRequestV2
var requestMap = make(map[string]string)
for _, actions := range blockActions {
for actionID, a := range actions {
if string(a.Type) == string(slack.METPlainTextInput) {
requestMap[actionID] = a.Value
}
if string(a.Type) == slack.OptTypeStatic {
requestMap[actionID] = a.SelectedOption.Value
}
}
}
desRequestMap, _ := json.Marshal(requestMap)
err := json.Unmarshal(desRequestMap, &createIncidentRequest)
if err != nil {
return nil, err
}
createIncidentRequest.CreatedBy = callback.User.ID
return &createIncidentRequest, nil
}

View File

@@ -2,6 +2,8 @@ package processor
import (
"fmt"
"github.com/spf13/viper"
"gorm.io/gorm"
"houston/common/util"
"houston/internal/processor/action"
"houston/model/incident"
@@ -192,16 +194,25 @@ type ViewSubmissionProcessor struct {
incidentUpdateRca *action.IncidentUpdateRcaAction
showIncidentSubmitAction *action.ShowIncidentsSubmitAction
incidentDuplicateAction *action.DuplicateIncidentAction
db *gorm.DB
}
func NewViewSubmissionProcessor(logger *zap.Logger, socketModeClient *socketmode.Client, incidentRepository *incident.Repository,
teamService *team.Repository, severityService *severity.Repository, tagService *tag.Repository, teamRepository *team.Repository,
slackbotClient *slackbot.Client) *ViewSubmissionProcessor {
func NewViewSubmissionProcessor(
logger *zap.Logger,
socketModeClient *socketmode.Client,
incidentRepository *incident.Repository,
teamService *team.Repository,
severityService *severity.Repository,
tagService *tag.Repository,
teamRepository *team.Repository,
slackbotClient *slackbot.Client,
db *gorm.DB,
) *ViewSubmissionProcessor {
return &ViewSubmissionProcessor{
logger: logger,
socketModeClient: socketModeClient,
incidentChannelMessageUpdateAction: action.NewIncidentChannelMessageUpdateAction(socketModeClient, logger, incidentRepository, teamService, severityService),
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, logger, incidentRepository, teamService, severityService, slackbotClient),
createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, logger, incidentRepository, teamService, severityService, slackbotClient, db),
assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, logger, incidentRepository),
updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, logger, incidentRepository, tagService, teamService, severityService),
incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, logger, incidentRepository, teamService, severityService, slackbotClient),
@@ -226,7 +237,12 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal
switch callbackId {
case util.StartIncidentSubmit:
{
vsp.createIncidentAction.CreateIncidentModalCommandProcessing(callback, request)
createIncidentV2Enabled := viper.GetBool("CREATE_INCIDENT_V2_ENABLED")
if createIncidentV2Enabled {
vsp.createIncidentAction.CreateIncidentModalCommandProcessingV2(callback, request)
} else {
vsp.createIncidentAction.CreateIncidentModalCommandProcessing(callback, request)
}
}
case util.AssignIncidentRoleSubmit:
{

View File

@@ -25,7 +25,7 @@ func NewIncidentRepository(logger *zap.Logger, gormClient *gorm.DB, severityServ
}
}
func (r *Repository) CreateIncident(request *CreateIncidentRequest) (*IncidentEntity, error) {
func (r *Repository) CreateIncidentEntity(request *CreateIncidentDTO) (*IncidentEntity, error) {
severityId, err := strconv.Atoi(request.Severity)
if err != nil {
return nil, fmt.Errorf("fetch channel conversationInfo failed. err: %v", err)

View File

@@ -31,7 +31,7 @@ func (j JSON) Value() (driver.Value, error) {
return json.RawMessage(j).MarshalJSON()
}
type CreateIncidentRequest struct {
type CreateIncidentDTO struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Severity string `json:"severity,omitempty"`

View File

@@ -64,3 +64,27 @@ func (authService *AuthService) checkIfManagerOrAdmin(
}
return strings.Contains(userRoles, string(Admin)), nil
}
func (authService *AuthService) checkIfAdminOrTeamMember(context *gin.Context, teamMembers []string) (bool, error) {
sessionToken := context.Request.Header.Get("X-Session-Token")
userEmail := context.Request.Header.Get("X-User-Email")
sessionResponse, err := authService.mjolnirClient.GetSessionResponse(sessionToken)
if err != nil || sessionResponse.StatusCode == 401 {
authService.logger.Error(fmt.Sprintf("error occurred while getting session data from mjolnir for %v", userEmail), zap.Error(err))
return false, nil
}
if sessionResponse.EmailId != userEmail {
authService.logger.Error(fmt.Sprintf("user email: %v does not match the email linked to the session token", userEmail))
return false, nil
}
if teamMembers == nil || len(teamMembers) == 0 {
return strings.Contains(strings.Join(sessionResponse.Roles, ","), string(Admin)), nil
}
userInfo, err := authService.slackClient.GetUserByEmail(userEmail)
if err != nil {
authService.logger.Error(fmt.Sprintf("User with email %v was not found in slack", userEmail), zap.Error(err))
return false, nil
}
return strings.Contains(strings.Join(sessionResponse.Roles, ","), string(Admin)) ||
strings.Contains(strings.Join(teamMembers, ","), userInfo.ID), nil
}

View File

@@ -0,0 +1,628 @@
package incident
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
slackClient "github.com/slack-go/slack"
"github.com/spf13/viper"
"go.uber.org/zap"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
"gorm.io/gorm"
"houston/common/util"
"houston/internal/processor/action/view"
"houston/model/incident"
"houston/model/severity"
"houston/model/team"
"houston/model/user"
request "houston/service/request"
service "houston/service/response"
"houston/service/slack"
"strconv"
"strings"
"time"
)
type IncidentServiceV2 struct {
logger *zap.Logger
db *gorm.DB
slackService *slack.SlackService
teamRepository *team.Repository
severityRepository *severity.Repository
incidentRepository *incident.Repository
userRepository *user.Repository
}
func NewIncidentServiceV2(logger *zap.Logger, db *gorm.DB) *IncidentServiceV2 {
teamRepository := team.NewTeamRepository(logger, db)
severityRepository := severity.NewSeverityRepository(logger, db)
incidentRepository := incident.NewIncidentRepository(logger, db, severityRepository)
userRepository := user.NewUserRepository(logger, db)
slackService := slack.NewSlackService(logger)
return &IncidentServiceV2{
logger: logger,
db: db,
slackService: slackService,
teamRepository: teamRepository,
severityRepository: severityRepository,
incidentRepository: incidentRepository,
userRepository: userRepository,
}
}
const logTag = "[create-incident-v2]"
/*
gets validated request
creates entry in DB, creates slack channel, posts incident summary, adds members, tags oncall, assigns responder
returns error if failure otherwise nil
*/
func (i *IncidentServiceV2) CreateIncident(
request request.CreateIncidentRequestV2,
source string,
blazeGroupChannelID string,
) (service.IncidentResponse, error) {
emptyResponse := service.IncidentResponse{}
// Create incident dto
i.logger.Info(fmt.Sprintf("%s received request to create incident: %+v", logTag, request))
teamEntity, severityEntity, err := getTeamAndSeverityEntity(i, request.TeamID, request.SeverityID)
if err != nil {
return emptyResponse, err
}
incidentDTO, err := buildCreateIncidentDTO(request, i.teamRepository, i.severityRepository, i.slackService)
if err != nil {
return emptyResponse, err
}
i.logger.Info(fmt.Sprintf("%s CreateIncidentDTO created", logTag))
// Save the incident to the database
incidentEntity, err := i.incidentRepository.CreateIncidentEntity(incidentDTO)
if err != nil {
i.logger.Error(fmt.Sprintf("%s Error while creating incident", logTag), zap.Error(err))
return emptyResponse, err
}
incidentName := incidentEntity.IncidentName
i.logger.Info(fmt.Sprintf("%s Incident entity created. Incident is: %s", logTag, incidentName))
// Create slack channel
// Call slack service to create a slack channel for incident
channel, err := i.slackService.CreateSlackChannel(incidentEntity.ID)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Error while crating slack channel", logTag, incidentName),
zap.Error(err),
)
return emptyResponse, err
}
i.logger.Info(fmt.Sprintf(
"%s [%s] Slack channel created. Channel name is %s", logTag, incidentName, channel.Name),
)
// Update channel details to incident entity
incidentEntity.SlackChannel = channel.ID
incidentEntity.IncidentName = channel.Name
err = i.incidentRepository.UpdateIncident(incidentEntity)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Failed to update the slack channel details in DB", logTag, incidentName))
return emptyResponse, err
}
i.logger.Info(fmt.Sprintf("%s [%s] Slack channel details updated to incident entity", logTag, incidentName))
// Post incident summary
// Call slack service, provide required message to be posted
incidentStatusEntity, err := getIncidentStatusEntity(i, incidentEntity.ID, incidentEntity.Status, incidentName)
if err != nil {
return emptyResponse, err
}
i.logger.Info(fmt.Sprintf("%s [%s] Team, Severity and IncidentStatus entity fetched successfully", logTag, incidentName))
err = i.slackService.SetChannelTopic(channel, teamEntity, severityEntity, incidentEntity)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Failed to set channel topic", logTag, incidentName),
zap.Error(err),
)
}
i.logger.Info(fmt.Sprintf("%s [%s] Channel topic is set", logTag, incidentName))
if source == "SLACK" {
go func() {
err := createIncidentWorkflow(i, channel, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, blazeGroupChannelID)
if err != nil {
return
}
}()
} else {
err = createIncidentWorkflow(i, channel, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, blazeGroupChannelID)
if err != nil {
return service.IncidentResponse{}, err
}
}
go postInWebhookSlackChannel(i, teamEntity, incidentEntity, severityEntity)
return service.ConvertToIncidentResponse(*incidentEntity), nil
}
func createIncidentWorkflow(
i *IncidentServiceV2,
channel *slackClient.Channel,
incidentEntity *incident.IncidentEntity,
teamEntity *team.TeamEntity,
severityEntity *severity.SeverityEntity,
incidentStatusEntity *incident.IncidentStatusEntity,
blazeGroupChannelID string,
) error {
incidentName := incidentEntity.IncidentName
timestamp, err := postIncidentSummary(
channel.ID,
incidentEntity,
teamEntity,
severityEntity,
incidentStatusEntity,
i.incidentRepository,
blazeGroupChannelID,
i.slackService,
)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Error in posting incident summary to slack channel", logTag, incidentName),
zap.Error(err),
)
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] Incident summary posted", logTag, incidentName))
// Update incident channel details
err = i.incidentRepository.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{
SlackChannel: channel.ID,
MessageTimeStamp: *timestamp,
IncidentId: incidentEntity.ID,
})
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Error in creating incident channel entry: %d", logTag, incidentName),
zap.Error(err),
)
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] Incident channel details updated to DB", logTag, incidentName))
// Add required members to channel
// Add incident creator to the channel
// Check if the user is a slack user and get the slack user id
isSlackUser, slackUserId := i.userRepository.IsAHoustonUser(incidentEntity.CreatedBy)
if isSlackUser {
// Add user who created the incident
err := i.slackService.InviteUsersToConversation(incidentEntity.SlackChannel, slackUserId)
if err != nil {
i.logger.Error(
fmt.Sprintf(
"%s [%s] Failed to add the incident creator %s to the channel: %s",
logTag, incidentName, incidentEntity.CreatedBy, incidentEntity.SlackChannel,
),
)
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] Incident creator is added to the slack channel", logTag, incidentName))
}
// Call addMembersToIncident(), provide channel, team name and severity
err = addMembersToIncident(channel, i, teamEntity, severityEntity, incidentName)
if err != nil {
_, err := i.slackService.PostMessage(
fmt.Sprintf("`"+"Failed to add some team members to the incident channel"+"`"),
false,
channel,
)
if err != nil {
i.logger.Debug(fmt.Sprintf("%s Failed to add members to the incident channel", logTag), zap.Error(err))
}
}
i.logger.Info(fmt.Sprintf("%s [%s] All the members got added to the incident channel", logTag, incidentName))
// Tag oncall
// Call slack service to tag the oncall, provide incident id and slack id to be tagged
err = tagPseOrDevOncallToIncident(channel, severityEntity, teamEntity, i)
if err != nil {
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] oncall us tagged to the incident", logTag, incidentName))
// Assign responder
// Call assignResponder(), provide incident id, team service should assign responder
err = assignResponderToIncident(
i,
incidentEntity,
teamEntity,
severityEntity,
incidentName,
)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Failed to assign responder to the incident", logTag, incidentName),
zap.Error(err),
)
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] Responder is assigned to the incident", logTag, incidentName))
// Post message about the SLA
if incidentEntity.SeverityId >= 3 && incidentEntity.Status <= 4 {
slaDate := time.Now().AddDate(0, 0, severityEntity.Sla).Format("02 Jan 2006")
message := fmt.Sprintf(
"SLA for this incident is `%v day(s)` and will be closed by `%s`",
severityEntity.Sla,
slaDate,
)
_, err := i.slackService.PostMessage(message, false, channel)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Failed to SLA information to the slack channel", logTag, incidentName), zap.Error(err))
return err
}
}
if viper.GetBool("ENABLE_GMEET") {
gmeet, err := createGmeetLink(i.logger, channel.Name)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Error while creating gmeet", logTag, incidentName), zap.Error(err))
} else {
_, err := i.slackService.PostMessage(fmt.Sprint("gmeet: ", gmeet), false, channel)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Failed to post Google Meet link: %s", logTag, incidentName, gmeet))
}
}
i.logger.Info(fmt.Sprintf("%s [%s] Google Meeting link posted to the channel %s", logTag, incidentName, gmeet))
}
return nil
}
func postIncidentSummary(
incidentChannelID string,
incidentEntity *incident.IncidentEntity,
teamEntity *team.TeamEntity,
severityEntity *severity.SeverityEntity,
incidentStatusEntity *incident.IncidentStatusEntity,
incidentRepository *incident.Repository,
blazeGroupChannelID string,
slackService *slack.SlackService,
) (*string, error) {
// Post incident summary to Blaze Group channel and incident channel
blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity)
color := util.GetColorBySeverity(incidentEntity.SeverityId)
if blazeGroupChannelID != "" {
timestamp, err := slackService.PostMessageBlocks(blazeGroupChannelID, blocks, color)
if err == nil {
err := incidentRepository.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{
SlackChannel: blazeGroupChannelID,
MessageTimeStamp: timestamp,
IncidentId: incidentEntity.ID,
})
if err != nil {
return nil, fmt.Errorf(
"failed to create incident channel entry for blazeGroupChannelID: %s", blazeGroupChannelID,
)
}
} else {
return nil, fmt.Errorf("error in posting summary to the blazeGroup channel %v", err)
}
}
timestamp, err := slackService.PostMessageBlocks(incidentChannelID, blocks, color)
if incidentEntity.MetaData != nil {
msgOption, noMetaDataError := util.BuildSlackTextMessageFromMetaData(incidentEntity.MetaData, true)
if noMetaDataError == nil {
_, err = slackService.PostMessageOption(incidentChannelID, msgOption)
if err != nil {
return nil, fmt.Errorf(
"error in posting metagada to the incident channel: %s. error: %v", incidentChannelID, err,
)
}
}
}
if err == nil {
err := incidentRepository.CreateIncidentChannelEntry(&incident.CreateIncidentChannelEntry{
SlackChannel: incidentChannelID,
MessageTimeStamp: timestamp,
IncidentId: incidentEntity.ID,
})
if err != nil {
return nil, fmt.Errorf(
"failed to create incident channel entry for channelID: %s", incidentChannelID,
)
}
} else {
return nil, fmt.Errorf(
"error in posting summary to the incident channel: %s. error: %v", incidentChannelID, err,
)
}
return &timestamp, nil
}
func addMembersToIncident(
channel *slackClient.Channel,
i *IncidentServiceV2,
teamEntity *team.TeamEntity,
severityEntity *severity.SeverityEntity,
incidentName string,
) error {
// 1. get list of team members, list of members by severity
var userIdList []string
userIdList = append(
append(
append(
append(userIdList, teamEntity.SlackUserIds...),
severityEntity.SlackUserIds...,
),
teamEntity.OncallHandle,
),
teamEntity.PseOncallHandle,
)
err := i.slackService.InviteUsersToConversation(channel.ID, userIdList[:]...)
if err != nil {
i.logger.Error(
fmt.Sprintf(
"%s [%s] Error in adding members [%+v] to the channel %s",
logTag, incidentName, userIdList, channel.Name,
),
zap.Error(err),
)
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] All the members are added to the slack channel", logTag, incidentName))
return nil
}
func assignResponderToIncident(
i *IncidentServiceV2,
incidentEntity *incident.IncidentEntity,
teamEntity *team.TeamEntity,
severityEntity *severity.SeverityEntity,
incidentName string,
) error {
// 1. find the responder (dev or pse oncall)
var responder, nonEmpty = getOncallOrResponderHandle(teamEntity, severityEntity)
// 2. updated responder in db
if nonEmpty {
var addIncidentRoleRequest = incident.AddIncidentRoleRequest{
UserId: responder,
Role: incident.Responder,
IncidentId: int(incidentEntity.ID),
CreatedById: incidentEntity.CreatedBy,
}
err := i.incidentRepository.UpsertIncidentRole(&addIncidentRoleRequest)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Failed to upsert incident_role", logTag, incidentName), zap.Error(err))
return err
}
i.logger.Info(fmt.Sprintf("%s [%s] Incident role upserted", logTag, incidentName))
_, err = i.slackService.PostMessageByChannelID(
fmt.Sprintf("<@%s> is assigned as *%s* by <@%s>", addIncidentRoleRequest.UserId, addIncidentRoleRequest.Role, addIncidentRoleRequest.CreatedById),
false,
incidentEntity.SlackChannel,
)
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Post response failed for AssignResponderToIncident", logTag, incidentName), zap.Error(err))
return err
}
}
return nil
}
func buildCreateIncidentDTO(
createIncRequest request.CreateIncidentRequestV2,
teamRepository *team.Repository,
severityRepository *severity.Repository,
slackService *slack.SlackService,
) (*incident.CreateIncidentDTO, error) {
var createIncidentRequest incident.CreateIncidentDTO
teamID, err := strconv.ParseUint(createIncRequest.TeamID, 10, 64)
if err != nil {
//todo handle error
}
teamEntity, err := teamRepository.FindTeamById(uint(teamID))
if err != nil {
return nil, err
}
severityID, err := strconv.ParseUint(createIncRequest.SeverityID, 10, 64)
if err != nil {
//todo handle error
}
severityEntity, err := severityRepository.FindSeverityById(uint(severityID))
if err != nil {
return nil, err
}
var rawJson, _ = json.Marshal([]request.CreateIncidentMetaData{createIncRequest.MetaData})
createdBy := slackService.GetSlackUserIdOrEmail(createIncRequest.CreatedBy)
createIncidentRequest.Title = createIncRequest.Title
createIncidentRequest.Description = createIncRequest.Description
createIncidentRequest.Severity = fmt.Sprintf("%d", severityEntity.ID)
createIncidentRequest.TeamId = fmt.Sprintf("%d", teamEntity.ID)
createIncidentRequest.Status = incident.Investigating
createIncidentRequest.CreatedBy = createdBy
createIncidentRequest.UpdatedBy = createdBy
createIncidentRequest.StartTime = time.Now()
createIncidentRequest.EnableReminder = false
createIncidentRequest.MetaData = rawJson
return &createIncidentRequest, nil
}
func getIncidentStatusEntity(
i *IncidentServiceV2,
incidentID,
statusID uint,
incidentName string,
) (*incident.IncidentStatusEntity, error) {
incidentStatusEntity, err := i.incidentRepository.FindIncidentStatusById(statusID)
if err != nil || incidentStatusEntity == nil {
i.logger.Error(
fmt.Sprintf(
"%s [%s] Error in finding incident statusID entity for statusID: %d in incidentID: %d",
logTag, incidentName, statusID, incidentID,
),
)
return nil, err
}
return incidentStatusEntity, nil
}
func getTeamAndSeverityEntity(
i *IncidentServiceV2,
team,
severity string,
) (*team.TeamEntity, *severity.SeverityEntity, error) {
teamID, err := strconv.ParseUint(team, 10, 64)
teamEntity, err := i.teamRepository.FindTeamById(uint(teamID))
if err != nil || teamEntity == nil {
i.logger.Error(
fmt.Sprintf("%s Error in finding team entity for team ID: %d", logTag, teamID),
)
return nil, nil, err
}
severityID, err := strconv.ParseUint(severity, 10, 64)
severityEntity, err := i.severityRepository.FindSeverityById(uint(severityID))
if err != nil || severityEntity == nil {
i.logger.Error(
fmt.Sprintf("%s Error in finding severity entity for severity ID: %d", logTag, severityID),
)
return nil, nil, err
}
return teamEntity, severityEntity, nil
}
func tagPseOrDevOncallToIncident(
channel *slackClient.Channel,
severityEntity *severity.SeverityEntity,
teamEntity *team.TeamEntity,
i *IncidentServiceV2,
) error {
incidentName := channel.Name
onCallToBeTagged, nonEmpty := getOncallOrResponderHandle(teamEntity, severityEntity)
if nonEmpty {
//oncall handles should already be added to channel
/*err := i.slackService.InviteUsersToConversation(channelId, onCallToBeTagged)
if err != nil {
i.logger.Error(fmt.Sprintf("%s Failed to invite oncall to the conversation"))
return err
}*/
ts, err := i.slackService.PostMessage(fmt.Sprintf("<@%s>", onCallToBeTagged), false, channel)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Failed to tag oncall in channel: %s", logTag, incidentName, channel.Name),
zap.Error(err),
)
return err
}
go func() {
time.Sleep(3 * time.Second)
msg, _, _, _ := i.slackService.GetConversationReplies(channel.ID, ts, 2)
if len(msg) > 1 {
//User id needs to sliced from `<@XXXXXXXXXXXX>` format to `XXXXXXXXXXXX`
err := i.slackService.InviteUsersToConversation(channel.ID, msg[1].Text[2:13])
if err != nil {
i.logger.Error(fmt.Sprintf("%s [%s] Failed to invite the user tagged by the oncall bot", logTag, incidentName))
return
}
i.logger.Info(fmt.Sprintf("%s [%s] User tagged by the oncall bot is invited to the incident channel", logTag, incidentName))
}
}()
}
return nil
}
func getOncallOrResponderHandle(teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) (string, bool) {
onCallOrResponderHandle := ""
if isPseIncident(teamEntity, severityEntity) {
onCallOrResponderHandle = teamEntity.PseOncallHandle
} else {
onCallOrResponderHandle = teamEntity.OncallHandle
}
return onCallOrResponderHandle, len(strings.TrimSpace(onCallOrResponderHandle)) > 0
}
func isPseIncident(teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) bool {
return len(strings.TrimSpace(teamEntity.PseOncallHandle)) > 0 && (severityEntity.Name == "Sev-2" || severityEntity.Name == "Sev-3")
}
func postInWebhookSlackChannel(
i *IncidentServiceV2,
teamEntity *team.TeamEntity,
incidentEntity *incident.IncidentEntity,
severityEntity *severity.SeverityEntity,
) {
incidentName := incidentEntity.IncidentName
if len(teamEntity.WebhookSlackChannel) == 0 {
return
}
msg := fmt.Sprintf(
"*<@%s>* started an Incident\n `%s(%s) %s` :slack: <#%s>: %s",
incidentEntity.CreatedBy,
severityEntity.Name,
severityEntity.Description,
teamEntity.Name,
incidentEntity.SlackChannel,
incidentEntity.Title,
)
_, err := i.slackService.PostMessageByChannelID(msg, false, teamEntity.WebhookSlackChannel)
if err != nil {
i.logger.Error(
fmt.Sprintf("%s [%s] Failed to post update in webhook slack channel: %s", logTag, incidentName, incidentEntity.SlackChannel),
zap.Error(err),
)
i.logger.Info(fmt.Sprintf("%s [%s] Update posted to the webhook slack channel: %s", logTag, incidentName, incidentEntity.SlackChannel))
return
}
}
func createGmeetLink(logger *zap.Logger, channelName string) (string, error) {
incidentName := channelName
calclient, err := calendar.NewService(context.Background(), option.WithCredentialsFile(viper.GetString("GMEET_CONFIG_FILE_PATH")))
if err != nil {
logger.Error(
fmt.Sprintf("%s [%s]Calander service creation failed, unable to read client secret file: ", logTag, incidentName),
zap.Error(err),
)
return "", err
}
t0 := time.Now().Format(time.RFC3339)
t1 := time.Now().Add(1 * time.Hour).Format(time.RFC3339)
event := &calendar.Event{
Summary: channelName,
Description: "Incident",
Start: &calendar.EventDateTime{
DateTime: t0,
},
End: &calendar.EventDateTime{
DateTime: t1,
},
ConferenceData: &calendar.ConferenceData{
CreateRequest: &calendar.CreateConferenceRequest{
RequestId: uuid.NewString(),
ConferenceSolutionKey: &calendar.ConferenceSolutionKey{
Type: "hangoutsMeet",
},
Status: &calendar.ConferenceRequestStatus{
StatusCode: "success",
},
},
},
}
calendarID := "primary" //use "primary"
event, err = calclient.Events.Insert(calendarID, event).ConferenceDataVersion(1).Do()
if err != nil {
logger.Error("Unable to create event. %v\n", zap.Error(err))
return "", err
}
calclient.Events.Delete(calendarID, event.Id).Do()
return event.HangoutLink, nil
}

View File

@@ -333,13 +333,13 @@ func (i *incidentService) CreateIncident(c *gin.Context) {
return
}
incidentRequest, err := i.buildCreateIncidentRequest(createIncRequest)
incidentDTO, err := i.buildCreateIncidentDTO(createIncRequest)
if err != nil {
c.JSON(http.StatusBadRequest, err)
}
// Save the incident to the database
incidentEntity, err := i.incidentRepository.CreateIncident(incidentRequest)
incidentEntity, err := i.incidentRepository.CreateIncidentEntity(incidentDTO)
if err != nil {
i.logger.Error("[Create incident Api] Error while creating incident", zap.Error(err))
return
@@ -351,7 +351,7 @@ func (i *incidentService) CreateIncident(c *gin.Context) {
incidentEntity.Status,
)
if err != nil {
i.logger.Error("[CIP] failed whilte getting team, severity and status", zap.Error(err))
i.logger.Error("[CIP] failed while getting team, severity and status", zap.Error(err))
return
}
@@ -396,6 +396,16 @@ func (i *incidentService) CreateIncident(c *gin.Context) {
i.logger.Error("[Create incident] Error while assigning responder to the incident ", zap.Error(err))
}
if incidentEntity.SeverityId >= 3 && incidentEntity.Status <= 4 {
slaDate := time.Now().AddDate(0, 0, severityEntity.Sla).Format("02 Jan 2006")
message := fmt.Sprintf("SLA for this incident is `%v day(s)` and will be closed by `%s`", severityEntity.Sla,
slaDate)
err := util.PostMessageToIncidentChannel(message, incidentEntity.SlackChannel, i.socketModeClient)
if err != nil {
return
}
}
incidentResponse := service.ConvertToIncidentResponse(*incidentEntity)
c.JSON(http.StatusOK, common.SuccessResponse(incidentResponse, http.StatusOK))
}
@@ -499,8 +509,8 @@ func (i *incidentService) createSlackChannel(incidentEntity *incident.IncidentEn
return &channelID, nil
}
func (i *incidentService) buildCreateIncidentRequest(createIncRequest request.CreateIncidentRequest) (*incident.CreateIncidentRequest, error) {
var createIncidentRequest incident.CreateIncidentRequest
func (i *incidentService) buildCreateIncidentDTO(createIncRequest request.CreateIncidentRequest) (*incident.CreateIncidentDTO, error) {
var createIncidentRequest incident.CreateIncidentDTO
teamEntity, err := i.teamRepository.FindTeamByTeamName(createIncRequest.TeamName)
if err != nil {
return nil, err

View File

@@ -14,6 +14,15 @@ type CreateIncidentRequest struct {
MetaData CreateIncidentMetaData `gorm:"json:metaData"`
}
type CreateIncidentRequestV2 struct {
Title string `json:"title"`
Description string `json:"description"`
SeverityID string `json:"severity"`
TeamID string `json:"type"`
CreatedBy string `json:"createdBy"`
MetaData CreateIncidentMetaData `json:"metaData"`
}
type CreateIncidentMetaData struct {
CustomerId uuid.UUID `json:"customerId"`
CustomerName string `json:"customerName"`

View File

@@ -0,0 +1,205 @@
package slack
import (
"fmt"
"github.com/slack-go/slack"
"github.com/slack-go/slack/socketmode"
"github.com/spf13/viper"
"go.uber.org/zap"
"houston/model/incident"
"houston/model/severity"
"houston/model/team"
"os"
"strconv"
"strings"
)
type SlackService struct {
SocketModeClient *socketmode.Client
logger *zap.Logger
}
func NewSlackService(logger *zap.Logger) *SlackService {
return &SlackService{createSocketModeClient(logger), logger}
}
const logTag = "[slack-service]"
func (s *SlackService) PostMessage(message string, escape bool, channel *slack.Channel) (string, error) {
msgOption := slack.MsgOptionText(message, escape)
_, timeStamp, err := s.SocketModeClient.PostMessage(channel.ID, msgOption)
if err != nil {
e := fmt.Sprintf("%s Failed to post message into channel: %s", logTag, channel.Name)
s.logger.Error(e, zap.Error(err))
return "", fmt.Errorf("%s - %+v", e, err)
}
return timeStamp, nil
}
func (s *SlackService) PostMessageOption(channelID string, messageOption slack.MsgOption) (string, error) {
_, timeStamp, err := s.SocketModeClient.PostMessage(channelID, messageOption)
if err != nil {
e := fmt.Sprintf("%s Failed to post message into channelID: %s", logTag, channelID)
s.logger.Error(e, zap.Error(err))
return "", fmt.Errorf("%s - %+v", e, err)
}
return timeStamp, nil
}
func (s *SlackService) PostMessageByChannelID(message string, escape bool, channelID string) (string, error) {
msgOption := slack.MsgOptionText(message, escape)
_, timeStamp, err := s.SocketModeClient.PostMessage(channelID, msgOption)
if err != nil {
e := fmt.Sprintf("%s Failed to post message into channel: %s", logTag, channelID)
s.logger.Error(e, zap.Error(err))
return "", fmt.Errorf("%s : %+v", e, err)
}
return timeStamp, nil
}
func (s *SlackService) PostMessageBlocks(channelID string, blocks slack.Blocks, color string) (string, error) {
att := slack.Attachment{Blocks: blocks, Color: color}
messageOption := slack.MsgOptionAttachments(att)
_, timestamp, err := s.SocketModeClient.PostMessage(channelID, messageOption)
if err != nil {
return "", err
}
return timestamp, nil
}
func (s *SlackService) GetConversationReplies(channelID string, timeStamp string, limit int) (msgs []slack.Message, hasMore bool, nextCursor string, err error) {
return s.SocketModeClient.GetConversationReplies(
&slack.GetConversationRepliesParameters{ChannelID: channelID, Timestamp: timeStamp, Limit: limit},
)
}
func (s *SlackService) SetChannelTopic(
channel *slack.Channel,
teamEntity *team.TeamEntity,
severityEntity *severity.SeverityEntity,
incidentEntity *incident.IncidentEntity,
) error {
topic := fmt.Sprintf(
"%s-%s(%s) Incident-%d | %s",
teamEntity.Name,
severityEntity.Name,
severityEntity.Description,
incidentEntity.ID,
incidentEntity.Title,
)
_, err := s.SocketModeClient.SetTopicOfConversation(channel.ID, topic)
if err != nil {
e := fmt.Sprintf("%s set topic on slack channel failed", logTag)
s.logger.Error(e, zap.String("channel_id", channel.Name), zap.Error(err))
return fmt.Errorf("%s : %+v", e, err)
}
s.logger.Info("set topic on slack channel successful", zap.String("channel_id", channel.Name))
return nil
}
func (s *SlackService) GetUserByEmail(userEmail string) (*slack.User, error) {
user, err := s.SocketModeClient.GetUserByEmail(userEmail)
if err != nil {
e := fmt.Sprintf("%s error in getting user by email: %s", logTag, userEmail)
s.logger.Error(e, zap.Error(err))
return nil, fmt.Errorf("%s : %+v", e, err)
}
return user, nil
}
func (s *SlackService) GetSlackUserIdOrEmail(email string) string {
userInfo, err := s.GetUserByEmail(email)
if err != nil {
return email
} else {
return userInfo.ID
}
}
func (s *SlackService) CreateSlackChannel(incidentId uint) (*slack.Channel, error) {
channel, err := createChannel(getIncidentChannelName(incidentId), s.SocketModeClient, s.logger)
if err != nil {
return nil, fmt.Errorf("%s failed to create Slack Channel for incident %d. error: %+v", logTag, incidentId, err)
}
return channel, nil
}
func (s *SlackService) InviteUsersToConversation(channelId string, userId ...string) error {
_, err := s.SocketModeClient.InviteUsersToConversation(channelId, userId...)
if err != nil {
e := fmt.Sprintf("%s failed to invite users: %+v to the channel: %s", logTag, userId, channelId)
s.logger.Error(e, zap.Error(err))
return fmt.Errorf("%s : %+v", e, err)
}
s.logger.Info(
"successfully invite users to conversation",
zap.String("channel_id", channelId), zap.Any("user_ids", userId),
)
return nil
}
func getIncidentChannelName(incidentID uint) string {
var channelPrefix string
if viper.GetString("env") != "prod" {
channelPrefix = "_test-issue-"
} else {
channelPrefix = "_houston-"
}
return channelPrefix + strconv.Itoa(int(incidentID))
}
func createChannel(channelName string, socketModeClient *socketmode.Client, logger *zap.Logger) (*slack.Channel, error) {
request := slack.CreateConversationParams{
ChannelName: channelName,
IsPrivate: false,
}
channel, err := socketModeClient.CreateConversation(request)
if err != nil {
e := fmt.Sprintf("%s failed to create slack channel with name: %s", logTag, channelName)
logger.Error(e, zap.Error(err))
return nil, fmt.Errorf("%s : %+v", e, err)
}
logger.Info("created slackbot channel successfully", zap.String("channel_name", channelName), zap.String("channel_id", channel.ID))
return channel, nil
}
func createSocketModeClient(logger *zap.Logger) *socketmode.Client {
appToken := viper.GetString("houston.slack.app.token")
if appToken == "" {
logger.Error("HOUSTON_SLACK_APP_TOKEN must be set.")
os.Exit(1)
}
if !strings.HasPrefix(appToken, "xapp-") {
logger.Error("HOUSTON_SLACK_APP_TOKEN must have the prefix \"xapp-\".")
os.Exit(1)
}
botToken := viper.GetString("houston.slack.bot.token")
if botToken == "" {
logger.Error("HOUSTON_SLACK_BOT_TOKEN must be set.")
os.Exit(1)
}
if !strings.HasPrefix(botToken, "xoxb-") {
logger.Error("HOUSTON_SLACK_BOT_TOKEN must have the prefix \"xoxb-\".")
os.Exit(1)
}
api := slack.New(
botToken,
slack.OptionDebug(false),
slack.OptionAppLevelToken(appToken),
)
client := socketmode.New(
api,
socketmode.OptionDebug(false),
)
return client
}

View File

@@ -258,7 +258,7 @@ func (t *TeamService) UpdateTeam(c *gin.Context) {
authResult, _ := t.authService.checkIfManagerOrAdmin(c, teamEntity.ManagerHandle, Admin, Manager)
if !authResult {
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is neither an admin nor the manager of %v team", userEmail, teamEntity.Name)), http.StatusUnauthorized, nil))
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is neither an admin nor a member of %v team", userEmail, teamEntity.Name)), http.StatusUnauthorized, nil))
return
}

View File

@@ -64,6 +64,24 @@ func ValidateCreateIncidentRequest(request service.CreateIncidentRequest) error
return nil
}
func ValidateCreateIncidentRequestV2(request service.CreateIncidentRequestV2) error {
if request.Title == "" || request.Description == "" {
return errors.New("title and description should be present in create request")
}
descriptionMaxLength := viper.GetInt("create-incident.description.max-length")
titleMaxLength := viper.GetInt("create-incident.title.max-length")
if len(request.Description) > descriptionMaxLength {
return errors.New(fmt.Sprintf("description should not be more than %v characters long", descriptionMaxLength))
}
if len(request.Title) > titleMaxLength {
return errors.New(fmt.Sprintf("title should not be more than %v characters long", titleMaxLength))
}
if request.CreatedBy == "" {
return errors.New("created By should be present")
}
return nil
}
func ValidateUpdateTeamRequest(request service.UpdateTeamRequest) error {
if request.Id == 0 {
return errors.New("id should be present in update request")