From 8e7619f9728b5ebcec45bc8358e6e88c95cb3b2d Mon Sep 17 00:00:00 2001 From: Gullipalli Chetan Kumar Date: Fri, 12 Jan 2024 14:24:19 +0530 Subject: [PATCH] TP-52454 : Created Zenduty integration (#348) * TP-52454| created zenduty integration * TP-52454| added migration script for external team table * TP-52454| added extra logs * TP-52454| modified logs * TP-52454|added extra logs * TP-52454| changed post url for zenduty * TP-52454| fixed bugs in zenduty client * TP-52454| created constants for environmental varibales * TP-52454| enabled zenduty if severity is less than or equal to the defined config --- Makefile | 3 + cmd/app/server.go | 4 +- common/util/constant.go | 1 + common/util/environment_constant.go | 11 +++ .../000011_add_external_team_schema.up.sql | 14 ++++ .../action/incident_update_type_action.go | 2 +- .../action/set_severity_command_action.go | 2 +- .../action/set_team_command_action.go | 2 +- model/externalTeam/entity.go | 24 ++++++ model/externalTeam/model.go | 16 ++++ pkg/alertClient/alert_client_interface.go | 11 +++ pkg/alertClient/zenduty_client_impl.go | 76 +++++++++++++++++++ pkg/alertClient/zenduty_client_test.go | 70 +++++++++++++++++ .../external_team_repository.go | 7 ++ .../external_team_repository_impl.go | 25 ++++++ service/alertService/alert_service_impl.go | 56 ++++++++++++++ .../alertService/alert_service_interface.go | 11 +++ service/alertService/alert_service_test.go | 60 +++++++++++++++ service/incident/incident_service_v2.go | 26 +++++++ service/request/incident_alert_request.go | 16 ++++ service/teamService/team_service_v2.go | 43 ++++++++--- .../teamService/team_service_v2_interface.go | 2 + service/teamService/team_service_v2_test.go | 44 +++++++++-- 23 files changed, 507 insertions(+), 19 deletions(-) create mode 100644 common/util/environment_constant.go create mode 100644 db/migration/000011_add_external_team_schema.up.sql create mode 100644 model/externalTeam/entity.go create mode 100644 model/externalTeam/model.go create mode 100644 pkg/alertClient/alert_client_interface.go create mode 100644 pkg/alertClient/zenduty_client_impl.go create mode 100644 pkg/alertClient/zenduty_client_test.go create mode 100644 repository/externalTeamRepo/external_team_repository.go create mode 100644 repository/externalTeamRepo/external_team_repository_impl.go create mode 100644 service/alertService/alert_service_impl.go create mode 100644 service/alertService/alert_service_interface.go create mode 100644 service/alertService/alert_service_test.go create mode 100644 service/request/incident_alert_request.go diff --git a/Makefile b/Makefile index e0436f1..330c94b 100644 --- a/Makefile +++ b/Makefile @@ -64,3 +64,6 @@ generatemocks: cd $(CURDIR)/model/incident_jira && minimock -i IncidentJiraRepository -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/service/google && minimock -i IDriveService -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/service/rca && minimock -i IRCAService -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/pkg/alertClient && minimock -i AlertClient -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/repository/externalTeamRepo && minimock -i IExternalTeamRepository -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/service/alertService && minimock -i IAlertService -s _mock.go -o $(CURDIR)/mocks diff --git a/cmd/app/server.go b/cmd/app/server.go index 7189f2b..cd8cfcd 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -17,6 +17,7 @@ import ( "houston/model/user" "houston/pkg/rest" "houston/pkg/slackbot" + "houston/repository/externalTeamRepo" rcaRepository "houston/repository/rca/impl" "houston/repository/rcaInput" "houston/service" @@ -86,7 +87,8 @@ func (s *Server) teamHandler(houstonGroup *gin.RouterGroup) { teamRepository := team.NewTeamRepository(s.db, logRepository) userRepository := user.NewUserRepository(s.db) slackService := slack.NewSlackService() - teamServiceV2 := teamService.NewTeamServiceV2(teamRepository, userRepository, slackService) + externalTeamRepository := externalTeamRepo.NewExternalTeamRepository(s.db) + teamServiceV2 := teamService.NewTeamServiceV2(teamRepository, userRepository, slackService, externalTeamRepository) teamHandlerV2 := handler.NewTeamHandler(s.gin, teamServiceV2) houstonGroup.GET("/teams", teamHandlerV2.HandleGetAllTeams) houstonGroup.GET("/teams/:id", teamHandlerV2.HandleGetTeamDetails) diff --git a/common/util/constant.go b/common/util/constant.go index c6e37a8..0c4a9a9 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -96,6 +96,7 @@ type DocumentServiceFileTypeKeys string // DocumentServiceProvider service names for uploading documents to cloud const ( DocumentServiceProvider = "SA_DOCUMENT_SERVICE" + ZendutyProvider = "ZENDUTY" ) const ( diff --git a/common/util/environment_constant.go b/common/util/environment_constant.go new file mode 100644 index 0000000..2d6964f --- /dev/null +++ b/common/util/environment_constant.go @@ -0,0 +1,11 @@ +package util + +const ( + DefaultZendutyTimeout = "DEFAULT_ZENDUTY_TIMEOUT" + ZendutyBaseUrl = "ZENDUTY_BASE_URL" + ZendutyAuthorizationToken = "ZENDUTY_AUTHORIZATION_TOKEN" + ZendutyIncidentUrl = "ZENDUTY_INCIDENT_URL" + SlackChannelBaseUrl = "SLACK_CHANNEL_BASE_URL" + AlertIntegrationEnabled = "ALERT_INTEGRATION_ENABLED" + AlertEnabledSeverity = "ALERT_ENABLED_SEVERITY" +) diff --git a/db/migration/000011_add_external_team_schema.up.sql b/db/migration/000011_add_external_team_schema.up.sql new file mode 100644 index 0000000..72efb7b --- /dev/null +++ b/db/migration/000011_add_external_team_schema.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS external_team +( + id SERIAL PRIMARY KEY, + external_team_id character varying, + external_team_name character varying(100) NOT NULL, + metadata jsonb, + provider_name character varying(50) NOT NULL, + team_id INTEGER NOT NULL REFERENCES team (id), + is_active BOOLEAN DEFAULT true, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + created_by character varying(100), + updated_by character varying(100) +); diff --git a/internal/processor/action/incident_update_type_action.go b/internal/processor/action/incident_update_type_action.go index 58b9a3f..bc384ab 100644 --- a/internal/processor/action/incident_update_type_action.go +++ b/internal/processor/action/incident_update_type_action.go @@ -150,7 +150,7 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateType(cal incidentUpdateTypeAction.incidentServiceV2.HandleKrakatoaWorkflow(incidentEntity) }() - + go incidentUpdateTypeAction.incidentServiceV2.SendAlert(incidentEntity) var payload interface{} incidentUpdateTypeAction.socketModeClient.Ack(*request, payload) } diff --git a/internal/processor/action/set_severity_command_action.go b/internal/processor/action/set_severity_command_action.go index 67d95af..fb855ab 100644 --- a/internal/processor/action/set_severity_command_action.go +++ b/internal/processor/action/set_severity_command_action.go @@ -140,7 +140,7 @@ func (action *SetSeverityCommandAction) setSeverity(cmd slack.SlashCommand, seve logger.Error(fmt.Sprintf("%s Error while assigning responder to the incident %+v", setSeverityActionLogTag, zap.Error(err))) return fmt.Errorf("severity is set to %s. Failed to assign responder post severity update", severity) } - + go appcontext.GetIncidentService().SendAlert(incidentEntity) return nil }) } diff --git a/internal/processor/action/set_team_command_action.go b/internal/processor/action/set_team_command_action.go index 8ecf748..d21291f 100644 --- a/internal/processor/action/set_team_command_action.go +++ b/internal/processor/action/set_team_command_action.go @@ -140,7 +140,7 @@ func (action *SetTeamCommandAction) setTeam(cmd slack.SlashCommand, teamName str topic := fmt.Sprintf("%s-%s(%s) Incident-%d | %s", teamEntity.Name, severityEntity.Name, severityEntity.Description, incidentEntity.ID, incidentEntity.Title) action.slackBot.SetChannelTopic(cmd.ChannelID, topic) }() - + go appcontext.GetIncidentService().SendAlert(incidentEntity) return nil }) } diff --git a/model/externalTeam/entity.go b/model/externalTeam/entity.go new file mode 100644 index 0000000..e0fcaff --- /dev/null +++ b/model/externalTeam/entity.go @@ -0,0 +1,24 @@ +package externalTeam + +import ( + "gorm.io/datatypes" + "time" +) + +type ExternalTeamEntity struct { + ID uint `gorm:"primaryKey"` + ExternalTeamID string `gorm:"column:external_team_id"` + ExternalTeamName string `gorm:"column:external_team_name"` + Metadata datatypes.JSON `gorm:"column:metadata"` + ProviderName string `gorm:"column:provider_name"` + TeamID uint `gorm:"column:team_id"` + IsActive bool `gorm:"column:is_active"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + CreatedBy string `gorm:"column:created_by"` + UpdatedBy string `gorm:"column:updated_by"` +} + +func (ExternalTeamEntity) TableName() string { + return "external_team" +} diff --git a/model/externalTeam/model.go b/model/externalTeam/model.go new file mode 100644 index 0000000..9c38027 --- /dev/null +++ b/model/externalTeam/model.go @@ -0,0 +1,16 @@ +package externalTeam + +import "gorm.io/datatypes" + +type ExternalTeamDTO struct { + ExternalTeamID string `json:"external_team_id,omitempty"` + ExternalTeamName string `json:"external_team_name,omitempty"` + Metadata datatypes.JSON `json:"metadata,omitempty"` + ProviderName string `json:"provider_name,omitempty"` + TeamID uint `json:"team_id,omitempty"` + IsActive bool `json:"is_active,omitempty"` +} + +type ExternalTeamMetadata struct { + ServiceId string `json:"service_id"` +} diff --git a/pkg/alertClient/alert_client_interface.go b/pkg/alertClient/alert_client_interface.go new file mode 100644 index 0000000..4cbb3a4 --- /dev/null +++ b/pkg/alertClient/alert_client_interface.go @@ -0,0 +1,11 @@ +package alertClient + +import service "houston/service/request" + +type AlertClient interface { + CreateIncident(alertRequest service.AlertRequest) error +} + +func NewAlertClient() AlertClient { + return NewZendutyClient() +} diff --git a/pkg/alertClient/zenduty_client_impl.go b/pkg/alertClient/zenduty_client_impl.go new file mode 100644 index 0000000..ed9f4c7 --- /dev/null +++ b/pkg/alertClient/zenduty_client_impl.go @@ -0,0 +1,76 @@ +package alertClient + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/common/util" + "houston/logger" + "houston/pkg/rest" + request "houston/service/request" + "io" + "net/http" + "time" +) + +type ZendutyClient struct { + RestClient rest.HttpRestClient + DefaultTimeout time.Duration + BaseURL string + AuthorizationToken string +} + +func NewZendutyClient() *ZendutyClient { + return &ZendutyClient{ + RestClient: rest.NewHttpRestClient(), + DefaultTimeout: viper.GetDuration(util.DefaultZendutyTimeout), + BaseURL: viper.GetString(util.ZendutyBaseUrl), + AuthorizationToken: viper.GetString(util.ZendutyAuthorizationToken), + } +} + +const createLogTag = "[create-zenduty-alert]" + +func (zendutyClient *ZendutyClient) CreateIncident(alertRequest request.AlertRequest) error { + fullURL := zendutyClient.BaseURL + viper.GetString(util.ZendutyIncidentUrl) + incidentName := alertRequest.Name + requestHeaders := map[string]string{"Authorization": zendutyClient.AuthorizationToken} + + zendutyAlertRequest := request.ZendutyAlertRequest{ + Title: alertRequest.Title, + Summary: alertRequest.Description, + Service: alertRequest.ExternalTeamMetadata.ServiceId, + } + payload, marshalError := json.Marshal(zendutyAlertRequest) + if marshalError != nil { + logger.Error(fmt.Sprintf("%s error in json marshalling for %s", createLogTag, incidentName), zap.Error(marshalError)) + return marshalError + } + currentResponse, clientErr := zendutyClient.RestClient.PostWithTimeout(fullURL, *bytes.NewBuffer(payload), + requestHeaders, zendutyClient.DefaultTimeout, nil) + if clientErr != nil { + logger.Error(fmt.Sprintf("%s error while posting to zenduty for %s", createLogTag, incidentName), zap.Error(clientErr)) + return clientErr + } + if currentResponse == nil || currentResponse.Body == nil { + logger.Error(fmt.Sprintf("%s response from zenduty is nil for %s", createLogTag, incidentName)) + return errors.New("response from zenduty is nil") + } + // Have to store the response details to track zenduty incidents in future + responseBody, err := io.ReadAll(currentResponse.Body) + if err != nil { + logger.Error(fmt.Sprintf("%s error while reading response body", createLogTag), zap.Error(err)) + } else { + logger.Info(fmt.Sprintf("%s response body from post request for %s is: %s", createLogTag, incidentName, string(responseBody))) + } + if currentResponse.StatusCode != http.StatusCreated { + logger.Error(fmt.Sprintf("%s error occured from zenduty with status code %d for %s", createLogTag, + currentResponse.StatusCode, incidentName)) + return errors.New("error occurred from zenduty") + } + logger.Info(fmt.Sprintf("%s successfully sent alert for %s", createLogTag, incidentName)) + return nil +} diff --git a/pkg/alertClient/zenduty_client_test.go b/pkg/alertClient/zenduty_client_test.go new file mode 100644 index 0000000..bbf0946 --- /dev/null +++ b/pkg/alertClient/zenduty_client_test.go @@ -0,0 +1,70 @@ +package alertClient + +import ( + "errors" + "github.com/stretchr/testify/suite" + "houston/logger" + "houston/mocks" + "houston/model/externalTeam" + request "houston/service/request" + "net/http" + "testing" +) + +type ZendutyClientSuite struct { + suite.Suite + zendutyClient *ZendutyClient + restClient *mocks.HttpRestClientMock +} + +func (suite *ZendutyClientSuite) SetupSuite() { + logger.InitLogger() + suite.restClient = mocks.NewHttpRestClientMock(suite.T()) + suite.zendutyClient = &ZendutyClient{ + RestClient: suite.restClient, + } +} + +func TestZendutyClient(t *testing.T) { + suite.Run(t, new(ZendutyClientSuite)) +} + +func (suite *ZendutyClientSuite) TestCreateIncident_RestClientError() { + suite.restClient.PostWithTimeoutMock.Return(nil, errors.New("restClientError")) + err := suite.zendutyClient.CreateIncident(getMockAlertRequest()) + suite.Error(err) + suite.EqualError(err, "restClientError") +} + +func (suite *ZendutyClientSuite) TestCreateIncident_RestClientNilResponse() { + suite.restClient.PostWithTimeoutMock.Return(nil, nil) + err := suite.zendutyClient.CreateIncident(getMockAlertRequest()) + suite.Error(err) +} + +func (suite *ZendutyClientSuite) TestCreateIncident_RestClientNilBody() { + suite.restClient.PostWithTimeoutMock.Return(&http.Response{}, nil) + err := suite.zendutyClient.CreateIncident(getMockAlertRequest()) + suite.Error(err) +} +func (suite *ZendutyClientSuite) TestCreateIncident_StatusCodeFailure() { + suite.restClient.PostWithTimeoutMock.Return(&http.Response{StatusCode: http.StatusInternalServerError}, nil) + err := suite.zendutyClient.CreateIncident(getMockAlertRequest()) + suite.Error(err) +} + +func (suite *ZendutyClientSuite) TestCreateIncident_Success() { + suite.restClient.PostWithTimeoutMock.Return(&http.Response{StatusCode: http.StatusCreated, Body: http.NoBody}, nil) + err := suite.zendutyClient.CreateIncident(getMockAlertRequest()) + suite.NoError(err) +} +func getMockAlertRequest() request.AlertRequest { + return request.AlertRequest{ + Name: "name", + Title: "title", + Description: "description", + ExternalTeamMetadata: externalTeam.ExternalTeamMetadata{ + ServiceId: "service", + }, + } +} diff --git a/repository/externalTeamRepo/external_team_repository.go b/repository/externalTeamRepo/external_team_repository.go new file mode 100644 index 0000000..11ab2dd --- /dev/null +++ b/repository/externalTeamRepo/external_team_repository.go @@ -0,0 +1,7 @@ +package externalTeamRepo + +import "houston/model/externalTeam" + +type IExternalTeamRepository interface { + GetExternalTeamEntityByTeamIdAndProvider(teamId uint, provider string) (*externalTeam.ExternalTeamEntity, error) +} diff --git a/repository/externalTeamRepo/external_team_repository_impl.go b/repository/externalTeamRepo/external_team_repository_impl.go new file mode 100644 index 0000000..1f31bc0 --- /dev/null +++ b/repository/externalTeamRepo/external_team_repository_impl.go @@ -0,0 +1,25 @@ +package externalTeamRepo + +import ( + "gorm.io/gorm" + "houston/model/externalTeam" +) + +type Repository struct { + gormClient *gorm.DB +} + +func NewExternalTeamRepository(gormClient *gorm.DB) *Repository { + return &Repository{ + gormClient: gormClient, + } +} + +func (r *Repository) GetExternalTeamEntityByTeamIdAndProvider(teamId uint, provider string) (*externalTeam.ExternalTeamEntity, error) { + var externalTeamEntity externalTeam.ExternalTeamEntity + err := r.gormClient.Where("team_id = ? AND provider_name = ?", teamId, provider).First(&externalTeamEntity).Error + if err != nil { + return nil, err + } + return &externalTeamEntity, nil +} diff --git a/service/alertService/alert_service_impl.go b/service/alertService/alert_service_impl.go new file mode 100644 index 0000000..850df26 --- /dev/null +++ b/service/alertService/alert_service_impl.go @@ -0,0 +1,56 @@ +package alertService + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/spf13/viper" + "go.uber.org/zap" + "houston/common/util" + "houston/logger" + "houston/model/externalTeam" + "houston/pkg/alertClient" + request "houston/service/request" + response "houston/service/response" +) + +type AlertService struct { + AlertClient alertClient.AlertClient +} + +func NewAlertService() *AlertService { + return &AlertService{ + AlertClient: alertClient.NewAlertClient(), + } +} + +func (alertService *AlertService) CreateIncidentAlert(incidentDTO response.IncidentResponse, + externalTeamDTO *externalTeam.ExternalTeamDTO) error { + + teamId := incidentDTO.TeamId + if !externalTeamDTO.IsActive { + logger.Info(fmt.Sprintf("External team is inactive for teamId: %d", teamId)) + return errors.New("external team integration is inactive") + } + var externalTeamMetadata externalTeam.ExternalTeamMetadata + err := json.Unmarshal(externalTeamDTO.Metadata, &externalTeamMetadata) + if err != nil { + logger.Error(fmt.Sprintf("Error while unmarshalling metadata of external team for teamId: %d", teamId), zap.Error(err)) + return err + } + slackChannelUrl := viper.GetString(util.SlackChannelBaseUrl) + incidentDTO.SlackChannel + updatedDescription := fmt.Sprintf("%s, channel - %s \n %s", incidentDTO.IncidentName, slackChannelUrl, incidentDTO.Description) + alertRequest := request.AlertRequest{ + Name: incidentDTO.IncidentName, + Title: incidentDTO.Title, + Description: updatedDescription, + ExternalTeamMetadata: externalTeamMetadata, + } + err = alertService.AlertClient.CreateIncident(alertRequest) + if err != nil { + logger.Error(fmt.Sprintf("Error while sending alert for teamId: %d", teamId)) + return err + } + logger.Info(fmt.Sprintf("Succesfully sent alert for teamId: %d", teamId)) + return nil +} diff --git a/service/alertService/alert_service_interface.go b/service/alertService/alert_service_interface.go new file mode 100644 index 0000000..3cce85f --- /dev/null +++ b/service/alertService/alert_service_interface.go @@ -0,0 +1,11 @@ +package alertService + +import ( + "houston/model/externalTeam" + response "houston/service/response" +) + +type IAlertService interface { + CreateIncidentAlert(incidentResponse response.IncidentResponse, + externalTeamDTO *externalTeam.ExternalTeamDTO) error +} diff --git a/service/alertService/alert_service_test.go b/service/alertService/alert_service_test.go new file mode 100644 index 0000000..f1a1141 --- /dev/null +++ b/service/alertService/alert_service_test.go @@ -0,0 +1,60 @@ +package alertService + +import ( + "errors" + "github.com/stretchr/testify/suite" + "houston/logger" + "houston/mocks" + "houston/model/externalTeam" + response "houston/service/response" + "testing" +) + +type AlertServiceSuite struct { + suite.Suite + alertService *AlertService + alertClient *mocks.AlertClientMock +} + +func (suite *AlertServiceSuite) SetupSuite() { + logger.InitLogger() + suite.alertClient = mocks.NewAlertClientMock(suite.T()) + suite.alertService = &AlertService{ + AlertClient: suite.alertClient, + } +} + +func TestAlertService(t *testing.T) { + suite.Run(t, new(AlertServiceSuite)) +} + +func (suite *AlertServiceSuite) TestCreateIncidentAlert_InactiveTeam() { + externalTeamDTO := &externalTeam.ExternalTeamDTO{IsActive: false} + err := suite.alertService.CreateIncidentAlert(response.IncidentResponse{}, externalTeamDTO) + suite.Error(err) +} + +func (suite *AlertServiceSuite) TestCreateIncidentAlert_UnmarshalError() { + externalTeamDTO1 := &externalTeam.ExternalTeamDTO{IsActive: true} + err1 := suite.alertService.CreateIncidentAlert(response.IncidentResponse{}, externalTeamDTO1) + suite.Error(err1) + + externalTeamDTO2 := &externalTeam.ExternalTeamDTO{IsActive: true, Metadata: []byte("invalid data")} + err2 := suite.alertService.CreateIncidentAlert(response.IncidentResponse{}, externalTeamDTO2) + suite.Error(err2) +} + +func (suite *AlertServiceSuite) TestCreateIncidentAlert_AlertClientError() { + externalTeamDTO := &externalTeam.ExternalTeamDTO{IsActive: true, Metadata: []byte(`{"service_id": "serviceId"}`)} + suite.alertClient.CreateIncidentMock.Return(errors.New("alertClientError")) + err := suite.alertService.CreateIncidentAlert(response.IncidentResponse{}, externalTeamDTO) + suite.Error(err) + suite.EqualError(err, "alertClientError") +} + +func (suite *AlertServiceSuite) TestCreateIncidentAlert_Success() { + externalTeamDTO := &externalTeam.ExternalTeamDTO{IsActive: true, Metadata: []byte(`{"service_id": "serviceId"}`)} + suite.alertClient.CreateIncidentMock.Return(nil) + err := suite.alertService.CreateIncidentAlert(response.IncidentResponse{}, externalTeamDTO) + suite.NoError(err) +} diff --git a/service/incident/incident_service_v2.go b/service/incident/incident_service_v2.go index c34a983..12947db 100644 --- a/service/incident/incident_service_v2.go +++ b/service/incident/incident_service_v2.go @@ -23,6 +23,8 @@ import ( "houston/pkg/atlassian/dto/response" "houston/pkg/conference" "houston/pkg/rest" + "houston/repository/externalTeamRepo" + "houston/service/alertService" conferenceService "houston/service/conference" incidentChannel "houston/service/incident_channel" "houston/service/incident_jira" @@ -31,6 +33,7 @@ import ( service "houston/service/response" common "houston/service/response/common" "houston/service/slack" + "houston/service/teamService" "math" "net/http" "regexp" @@ -51,6 +54,8 @@ type IncidentServiceV2 struct { krakatoaService krakatoa.IKrakatoaService calendarService conferenceService.ICalendarService incidentJiraService incident_jira.IncidentJiraService + teamServiceV2 teamService.ITeamServiceV2 + alertService alertService.IAlertService } /* @@ -69,6 +74,7 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 { krakatoaService := krakatoa.NewKrakatoaService() calendarActions := conference.GetCalendarActions() calendarService := conferenceService.NewCalendarService(calendarActions) + teamServiceV2 := teamService.NewTeamServiceV2(teamRepository, userRepository, slackService, externalTeamRepo.NewExternalTeamRepository(db)) return &IncidentServiceV2{ db: db, slackService: slackService, @@ -80,6 +86,8 @@ func NewIncidentServiceV2(db *gorm.DB) *IncidentServiceV2 { krakatoaService: krakatoaService, calendarService: calendarService, incidentJiraService: incident_jira.NewIncidentJiraService(incidentJiraModel.NewIncidentJiraRepo(db)), + teamServiceV2: teamServiceV2, + alertService: alertService.NewAlertService(), } } @@ -171,6 +179,8 @@ func (i *IncidentServiceV2) CreateIncident( }() go postInWebhookSlackChannel(i, teamEntity, incidentEntity, severityEntity) + go i.SendAlert(incidentEntity) + return service.ConvertToIncidentResponse(*incidentEntity), nil } @@ -1312,6 +1322,7 @@ func (i *IncidentServiceV2) UpdateSeverityId( logger.Error(fmt.Sprintf("%s error in update severity workflow", updateLogTag), zap.Error(err)) return err } + go i.SendAlert(incidentEntity) } } return nil @@ -1574,6 +1585,7 @@ func (i *IncidentServiceV2) UpdateTeamId( logger.Error(fmt.Sprintf("%s error in update team id workflow", updateLogTag), zap.Error(err)) return err } + go i.SendAlert(incidentEntity) go i.HandleKrakatoaWorkflow(incidentEntity) } } @@ -1853,3 +1865,17 @@ func (i *IncidentServiceV2) addBookMarkAndPostMessageInSlack(incidentName string } logger.Info(fmt.Sprintf("%s [%s] Conference link posted to the incidentChannel %s", logTag, incidentName, conferenceLink)) } +func (i *IncidentServiceV2) SendAlert(incidentEntity *incident.IncidentEntity) { + if !viper.GetBool(util.AlertIntegrationEnabled) || incidentEntity.SeverityId > viper.GetUint(util.AlertEnabledSeverity) { + return + } + externalTeamDTO, err := i.teamServiceV2.GetExternalTeam(incidentEntity.TeamId, util.ZendutyProvider) + if err != nil { + return + } + incidentDTO := service.ConvertToIncidentResponse(*incidentEntity) + err = i.alertService.CreateIncidentAlert(incidentDTO, externalTeamDTO) + if err != nil { + logger.Error(fmt.Sprintf("Error occurred while sending alert for incident: %d", incidentEntity.ID)) + } +} diff --git a/service/request/incident_alert_request.go b/service/request/incident_alert_request.go new file mode 100644 index 0000000..a059cdb --- /dev/null +++ b/service/request/incident_alert_request.go @@ -0,0 +1,16 @@ +package service + +import "houston/model/externalTeam" + +type AlertRequest struct { + Name string + Title string + Description string + ExternalTeamMetadata externalTeam.ExternalTeamMetadata +} + +type ZendutyAlertRequest struct { + Title string `json:"title"` + Summary string `json:"summary"` + Service string `json:"service"` +} diff --git a/service/teamService/team_service_v2.go b/service/teamService/team_service_v2.go index a32d243..f52d417 100644 --- a/service/teamService/team_service_v2.go +++ b/service/teamService/team_service_v2.go @@ -4,29 +4,33 @@ import ( "errors" "fmt" "go.uber.org/zap" + "gorm.io/gorm" "houston/common/util" "houston/logger" "houston/model/customErrors" + externalTeam "houston/model/externalTeam" "houston/model/team" "houston/model/user" + "houston/repository/externalTeamRepo" service "houston/service/response" "houston/service/slack" "sort" ) type TeamServiceV2 struct { - teamRepository team.ITeamRepository - userRepository user.IUserRepository - slackService slack.ISlackService + teamRepository team.ITeamRepository + userRepository user.IUserRepository + slackService slack.ISlackService + externalTeamRepository externalTeamRepo.IExternalTeamRepository } -func NewTeamServiceV2(teamRepository team.ITeamRepository, - userRepository user.IUserRepository, - slackService slack.ISlackService) *TeamServiceV2 { +func NewTeamServiceV2(teamRepository team.ITeamRepository, userRepository user.IUserRepository, + slackService slack.ISlackService, externalTeamRepository externalTeamRepo.IExternalTeamRepository) *TeamServiceV2 { return &TeamServiceV2{ - teamRepository: teamRepository, - userRepository: userRepository, - slackService: slackService, + teamRepository: teamRepository, + userRepository: userRepository, + slackService: slackService, + externalTeamRepository: externalTeamRepository, } } @@ -168,3 +172,24 @@ func getOrderedUserResponses(userResponses []service.UserResponse, managerID str }) return userResponses } + +func (teamService *TeamServiceV2) GetExternalTeam(teamId uint, provider string) (*externalTeam.ExternalTeamDTO, error) { + externalTeamEntity, err := teamService.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProvider(teamId, provider) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + logger.Info(fmt.Sprintf("external team not found for teamId: %d, provider: %s", teamId, provider)) + } else { + logger.Error(fmt.Sprintf("error in fetching external team for teamId: %d, provider: %s", teamId, provider), zap.Error(err)) + } + return nil, err + } + externalTeamDTO := externalTeam.ExternalTeamDTO{ + ExternalTeamID: externalTeamEntity.ExternalTeamID, + ExternalTeamName: externalTeamEntity.ExternalTeamName, + Metadata: externalTeamEntity.Metadata, + ProviderName: externalTeamEntity.ProviderName, + TeamID: externalTeamEntity.TeamID, + IsActive: externalTeamEntity.IsActive, + } + return &externalTeamDTO, nil +} diff --git a/service/teamService/team_service_v2_interface.go b/service/teamService/team_service_v2_interface.go index 36bea1b..0d54f5a 100644 --- a/service/teamService/team_service_v2_interface.go +++ b/service/teamService/team_service_v2_interface.go @@ -1,10 +1,12 @@ package teamService import ( + "houston/model/externalTeam" service "houston/service/response" ) type ITeamServiceV2 interface { GetTeamDetails(teamId uint) (*service.TeamResponse, error) GetAllTeams() ([]service.TeamResponse, error) + GetExternalTeam(teamId uint, provider string) (*externalTeam.ExternalTeamDTO, error) } diff --git a/service/teamService/team_service_v2_test.go b/service/teamService/team_service_v2_test.go index dd9b199..71f127f 100644 --- a/service/teamService/team_service_v2_test.go +++ b/service/teamService/team_service_v2_test.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" "houston/logger" "houston/mocks" + "houston/model/externalTeam" "houston/model/team" "houston/model/user" service "houston/service/response" @@ -17,11 +18,12 @@ import ( type TeamServiceV2Suite struct { suite.Suite - controller *minimock.Controller - teamRepository *mocks.ITeamRepositoryMock - userRepository *mocks.IUserRepositoryMock - slackService *mocks.ISlackServiceMock - teamService *TeamServiceV2 + controller *minimock.Controller + teamRepository *mocks.ITeamRepositoryMock + userRepository *mocks.IUserRepositoryMock + slackService *mocks.ISlackServiceMock + externalTeamRepository *mocks.IExternalTeamRepositoryMock + teamService *TeamServiceV2 } func (suite *TeamServiceV2Suite) SetupSuite() { @@ -31,7 +33,8 @@ func (suite *TeamServiceV2Suite) SetupSuite() { suite.teamRepository = mocks.NewITeamRepositoryMock(suite.controller) suite.userRepository = mocks.NewIUserRepositoryMock(suite.controller) suite.slackService = mocks.NewISlackServiceMock(suite.controller) - suite.teamService = NewTeamServiceV2(suite.teamRepository, suite.userRepository, suite.slackService) + suite.externalTeamRepository = mocks.NewIExternalTeamRepositoryMock(suite.controller) + suite.teamService = NewTeamServiceV2(suite.teamRepository, suite.userRepository, suite.slackService, suite.externalTeamRepository) } func TestTeamServiceV2(t *testing.T) { @@ -198,3 +201,32 @@ func (suite *TeamServiceV2Suite) TestGetAllTeams_Failure() { assert.Nil(suite.T(), teamResponses) assert.EqualError(suite.T(), err, "error in fetching all teams") } + +func (suite *TeamServiceV2Suite) TestGetExternalTeam_Failure() { + suite.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProviderMock.Return(nil, errors.New("error")) + + externalTeamDTO, err := suite.teamService.GetExternalTeam(1, "zenduty") + + assert.Nil(suite.T(), externalTeamDTO) + assert.EqualError(suite.T(), err, "error") + +} + +func (suite *TeamServiceV2Suite) TestGetExternalTeam_Success() { + suite.externalTeamRepository.GetExternalTeamEntityByTeamIdAndProviderMock.When(1, "zenduty"). + Then(&externalTeam.ExternalTeamEntity{ + ExternalTeamID: "uuid", + ExternalTeamName: "name", + Metadata: nil, + ProviderName: "zenduty", + TeamID: 1, + IsActive: true, + }, nil) + + externalTeamDTO, err := suite.teamService.GetExternalTeam(1, "zenduty") + + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), "zenduty", externalTeamDTO.ProviderName) + assert.Equal(suite.T(), uint(1), externalTeamDTO.TeamID) + assert.Equal(suite.T(), true, externalTeamDTO.IsActive) +}