TP-40559 : Incident logs (#227) (#244)

* TP-40559 : Incident logs (#227)

* TP-40559 : Adding BeforeUpdate and AfterUpdate hook in Incident Entity

* TP-40936 - Added deep compare util function

* TP-40559 | Added entity and repo for logs

* TP-40559 : Added log entry support for incident level updates

* Fix zero diff issue

* Added lowercase json parameter names

* TP-40559 : Added logs for team level updates

* Initialize log service

* TP-41640 : Added api to fetch logs for particular incident/team

* Before create and after create

* Convert 2 logs to one on incident creation

* Log id populate:

* Add populate for team id

* Branch update changes

* Typo changes

* PR REVIEW CHANGES

* Nil fix

* Fix order issue

* TP-43841 | Updating fetch users from conversation to differentiate memeber and others (#245)

* Build fix

* Added migration script for logs

---------

Co-authored-by: Sriram Bhargav <sriram.bhargav@navi.com>
This commit is contained in:
Vijay Joshi
2023-10-19 15:47:04 +05:30
committed by GitHub
parent 70ee05ec76
commit 842966cb2f
18 changed files with 576 additions and 69 deletions

View File

@@ -2,24 +2,24 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"houston/internal/cron"
"houston/internal/diagnostic"
"houston/internal/processor"
"houston/internal/resolver"
"houston/model/incident"
"houston/model/severity"
"houston/model/shedlock"
"houston/model/tag"
"houston/model/team"
"houston/model/user"
"houston/pkg/slackbot"
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode" "github.com/slack-go/slack/socketmode"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"houston/internal/cron"
"houston/internal/diagnostic"
"houston/internal/processor"
"houston/internal/resolver"
"houston/model/incident"
"houston/model/log"
"houston/model/severity"
"houston/model/shedlock"
"houston/model/tag"
"houston/model/team"
"houston/model/user"
"houston/pkg/slackbot"
) )
type slackHandler struct { type slackHandler struct {
@@ -35,9 +35,10 @@ type slackHandler struct {
func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler { func NewSlackHandler(logger *zap.Logger, gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler {
severityService := severity.NewSeverityRepository(logger, gormClient) severityService := severity.NewSeverityRepository(logger, gormClient)
incidentService := incident.NewIncidentRepository(logger, gormClient, severityService) logRepository := log.NewLogRepository(logger, gormClient)
tagService := tag.NewTagRepository(logger, gormClient) tagService := tag.NewTagRepository(logger, gormClient)
teamService := team.NewTeamRepository(logger, gormClient) teamService := team.NewTeamRepository(logger, gormClient, logRepository)
incidentService := incident.NewIncidentRepository(logger, gormClient, severityService, logRepository, teamService, socketModeClient)
userService := user.NewUserRepository(logger, gormClient) userService := user.NewUserRepository(logger, gormClient)
shedlockService := shedlock.NewShedlockRepository(logger, gormClient) shedlockService := shedlock.NewShedlockRepository(logger, gormClient)
slackbotClient := slackbot.NewSlackClient(logger, socketModeClient) slackbotClient := slackbot.NewSlackClient(logger, socketModeClient)

View File

@@ -43,6 +43,7 @@ func (s *Server) Handler(houstonGroup *gin.RouterGroup) {
s.teamHandler(houstonGroup) s.teamHandler(houstonGroup)
s.severityHandler(houstonGroup) s.severityHandler(houstonGroup)
s.incidentHandler(houstonGroup) s.incidentHandler(houstonGroup)
s.logHandler(houstonGroup)
s.usersHandler(houstonGroup) s.usersHandler(houstonGroup)
s.filtersHandler(houstonGroup) s.filtersHandler(houstonGroup)
@@ -141,6 +142,11 @@ func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) {
houstonGroup.GET("/teamIncidents/:teamId", incidentHandler.GetTeamIncidents) houstonGroup.GET("/teamIncidents/:teamId", incidentHandler.GetTeamIncidents)
} }
func (s *Server) logHandler(houstonGroup *gin.RouterGroup) {
logHandler := service.NewLogService(s.gin, s.logger, s.db)
houstonGroup.GET("/logs/:log_type/:id", logHandler.GetLogs)
}
func (s *Server) usersHandler(houstonGroup *gin.RouterGroup) { func (s *Server) usersHandler(houstonGroup *gin.RouterGroup) {
houstonClient := NewHoustonClient(s.logger) houstonClient := NewHoustonClient(s.logger)
slackClient := slackbot.NewSlackClient(s.logger, houstonClient.socketModeClient) slackClient := slackbot.NewSlackClient(s.logger, houstonClient.socketModeClient)

View File

@@ -141,3 +141,11 @@ func PostMessageToIncidentChannel(message string, channelId string, client *sock
_, _, errMessage := client.PostMessage(channelId, msgOption) _, _, errMessage := client.PostMessage(channelId, msgOption)
return errMessage return errMessage
} }
func ConvertSliceToMapOfString(input []string) map[string]string {
output := make(map[string]string)
for _, item := range input {
output[item] = item
}
return output
}

View File

@@ -0,0 +1,9 @@
CREATE TABLE if not exists log
(
id SERIAL PRIMARY KEY,
relation_name character varying(255),
record_id integer,
created_at timestamp without time zone,
changes jsonb,
user_info jsonb
);

View File

@@ -1,8 +1,13 @@
package incident package incident
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/slack-go/slack/socketmode"
"houston/model/log"
"houston/model/severity" "houston/model/severity"
"houston/model/team"
utils "houston/service/utils"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -15,13 +20,25 @@ type Repository struct {
logger *zap.Logger logger *zap.Logger
gormClient *gorm.DB gormClient *gorm.DB
severityRepository *severity.Repository severityRepository *severity.Repository
logRepository *log.Repository
teamRepository *team.Repository
socketModeClient *socketmode.Client
} }
func NewIncidentRepository(logger *zap.Logger, gormClient *gorm.DB, severityService *severity.Repository) *Repository { var valueBeforeUpdate IncidentEntity
var valueAfterUpdate IncidentEntity
var valueBeforeCreate IncidentEntity
var valueAfterCreate IncidentEntity
var differences []utils.Difference
func NewIncidentRepository(logger *zap.Logger, gormClient *gorm.DB, severityService *severity.Repository, logRepository *log.Repository, teamRepository *team.Repository, socketModeClient *socketmode.Client) *Repository {
return &Repository{ return &Repository{
logger: logger, logger: logger,
gormClient: gormClient, gormClient: gormClient,
severityRepository: severityService, severityRepository: severityService,
logRepository: logRepository,
teamRepository: teamRepository,
socketModeClient: socketModeClient,
} }
} }
@@ -61,12 +78,141 @@ func (r *Repository) CreateIncidentEntity(request *CreateIncidentDTO) (*Incident
return incidentEntity, nil return incidentEntity, nil
} }
func (i *IncidentEntity) AfterCreate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("AfterUpdate executed at: %v", time.Now()))
valueBeforeCreate = IncidentEntity{}
valueAfterCreate = IncidentEntity{}
if err := tx.First(&valueAfterCreate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity after created is: %s", valueAfterCreate))
differences = utils.DeepCompare(valueBeforeCreate, valueAfterCreate)
return nil
}
func (i *IncidentEntity) BeforeUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("BeforeUpdate executed at: %v", time.Now()))
valueBeforeUpdate = IncidentEntity{}
if err := tx.First(&valueBeforeUpdate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity before update is: %s", valueBeforeUpdate))
return nil
}
func (i *IncidentEntity) AfterUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("AfterUpdate executed at: %v", time.Now()))
valueAfterUpdate = IncidentEntity{}
if err := tx.First(&valueAfterUpdate, i.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("incident entity after updated is: %s", valueAfterUpdate))
differences = append(differences, utils.DeepCompare(valueBeforeUpdate, valueAfterUpdate)...)
return nil
}
func (r *Repository) processDiffIds() []byte {
var jsonDiff []byte
for index := range differences {
switch differences[index].Attribute {
case "Status":
if differences[index].From != "" {
statusIdString, _ := strconv.Atoi(differences[index].From)
statusEntity, _ := r.GetIncidentStatusNameByStatus(uint(statusIdString))
if statusEntity != nil {
differences[index].From = statusEntity.Name
}
}
statusIdString, _ := strconv.Atoi(differences[index].To)
statusEntity, _ := r.GetIncidentStatusNameByStatus(uint(statusIdString))
differences[index].To = statusEntity.Name
case "SeverityId":
if differences[index].From != "" {
severityIdString, _ := strconv.Atoi(differences[index].From)
severityEntity, _ := r.severityRepository.FindSeverityById(uint(severityIdString))
if severityEntity != nil {
differences[index].From = severityEntity.Name
}
}
severityIdString, _ := strconv.Atoi(differences[index].To)
severityEntity, _ := r.severityRepository.FindSeverityById(uint(severityIdString))
differences[index].To = severityEntity.Name
case "TeamId":
if differences[index].From != "" {
teamIdString, _ := strconv.Atoi(differences[index].From)
teamEntity, _ := r.teamRepository.FindTeamById(uint(teamIdString))
if teamEntity != nil {
differences[index].From = teamEntity.Name
}
}
teamIdString, _ := strconv.Atoi(differences[index].To)
teamEntity, _ := r.teamRepository.FindTeamById(uint(teamIdString))
if teamEntity != nil {
differences[index].To = teamEntity.Name
}
}
}
jsonDiff, _ = json.Marshal(differences)
return jsonDiff
}
func (r *Repository) processUserInfo() ([]byte, error) {
user, err := r.socketModeClient.GetUsersInfo(valueAfterUpdate.UpdatedBy)
if err != nil {
errorMessage := fmt.Sprintf("failed to get user info from slack for userID: %s", valueAfterUpdate.UpdatedBy)
r.logger.Error(errorMessage)
return nil, fmt.Errorf("%s. Error: %v", errorMessage, err)
}
if len(*user) != 0 {
userData := (*user)[0]
userInfo := log.UserInfo{
Id: userData.ID,
Email: userData.Profile.Email,
Name: userData.Profile.RealName,
}
return json.Marshal(userInfo)
}
return nil, fmt.Errorf("%s is not a valid user", valueAfterUpdate.UpdatedBy)
}
func (r *Repository) captureLogs() {
if differences != nil && len(differences) > 0 {
jsonUser, _ := r.processUserInfo()
jsonDiff := r.processDiffIds()
r.logRepository.CreateLog(
&log.LogEntity{
CreatedAt: time.Now(),
RelationName: "incident",
RecordId: valueAfterUpdate.ID,
UserInfo: jsonUser,
Changes: jsonDiff,
})
differences = []utils.Difference{}
}
}
func (r *Repository) UpdateIncident(incidentEntity *IncidentEntity) error { func (r *Repository) UpdateIncident(incidentEntity *IncidentEntity) error {
result := r.gormClient.Updates(incidentEntity) result := r.gormClient.Updates(incidentEntity)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
r.captureLogs()
return nil return nil
} }

24
model/log/entity.go Normal file
View File

@@ -0,0 +1,24 @@
package log
import (
"gorm.io/datatypes"
"time"
)
type LogEntity struct {
CreatedAt time.Time `gorm:"column:created_at"`
RelationName string `gorm:"column:relation_name"`
RecordId uint `gorm:"column:record_id"`
UserInfo datatypes.JSON `gorm:"column:user_info"`
Changes datatypes.JSON `gorm:"column:changes"`
}
func (LogEntity) TableName() string {
return "log"
}
type UserInfo struct {
Id string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}

48
model/log/log.go Normal file
View File

@@ -0,0 +1,48 @@
package log
import (
"go.uber.org/zap"
"gorm.io/gorm"
)
type Repository struct {
logger *zap.Logger
gormClient *gorm.DB
}
func NewLogRepository(logger *zap.Logger, gormClient *gorm.DB) *Repository {
return &Repository{
logger: logger,
gormClient: gormClient,
}
}
func (r *Repository) CreateLog(logEntity *LogEntity) (*LogEntity, error) {
result := r.gormClient.Create(logEntity)
if result.Error != nil {
return nil, result.Error
}
return logEntity, nil
}
func (r *Repository) FetchLogsByRelationNameAndRecordId(relationName string, recordId uint) ([]LogEntity, error) {
var query = r.gormClient.Model([]LogEntity{})
var logEntity []LogEntity
if len(relationName) != 0 {
query = query.Where("relation_name = ?", relationName)
}
query = query.Where("record_id = ?", recordId)
query = query.Order("created_at ASC")
result := query.Find(&logEntity)
if result.Error != nil {
return nil, result.Error
}
return logEntity, nil
}

View File

@@ -1,19 +1,30 @@
package team package team
import ( import (
"encoding/json"
"fmt"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
"houston/model/log"
utils "houston/service/utils"
"time"
) )
type Repository struct { type Repository struct {
logger *zap.Logger logger *zap.Logger
gormClient *gorm.DB gormClient *gorm.DB
logRepository *log.Repository
} }
func NewTeamRepository(logger *zap.Logger, gormClient *gorm.DB) *Repository { var valueBeforeUpdate TeamEntity
var valueAfterUpdate TeamEntity
var differences []utils.Difference
func NewTeamRepository(logger *zap.Logger, gormClient *gorm.DB, logRepository *log.Repository) *Repository {
return &Repository{ return &Repository{
logger: logger, logger: logger,
gormClient: gormClient, gormClient: gormClient,
logRepository: logRepository,
} }
} }
@@ -63,11 +74,57 @@ func (r *Repository) FindTeamByTeamName(teamName string) (*TeamEntity, error) {
return &teamEntity, nil return &teamEntity, nil
} }
func (t *TeamEntity) BeforeUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("BeforeUpdate executed at: %v", time.Now()))
valueBeforeUpdate = TeamEntity{}
if err := tx.First(&valueBeforeUpdate, t.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("team entity before update is: %s", valueBeforeUpdate))
return nil
}
func (t *TeamEntity) AfterUpdate(tx *gorm.DB) (err error) {
println(fmt.Sprintf("AfterUpdate executed at: %v", time.Now()))
valueAfterUpdate = TeamEntity{}
if err := tx.First(&valueAfterUpdate, t.ID).Error; err != nil {
return err
}
println(fmt.Sprintf("team entity after updated is: %s", valueAfterUpdate))
differences = utils.DeepCompare(valueBeforeUpdate, valueAfterUpdate)
return nil
}
func (r *Repository) CaptureLogs() {
if differences != nil && len(differences) > 0 {
jsonDiff, _ := json.Marshal(differences)
jsonUser, _ := json.Marshal(log.UserInfo{
Id: "",
Email: valueAfterUpdate.UpdatedBy,
Name: "",
})
r.logRepository.CreateLog(
&log.LogEntity{
CreatedAt: time.Now(),
RelationName: "team",
RecordId: valueAfterUpdate.ID,
Changes: jsonDiff,
UserInfo: jsonUser,
})
}
return
}
func (r *Repository) UpdateTeam(teamEntity *TeamEntity) error { func (r *Repository) UpdateTeam(teamEntity *TeamEntity) error {
result := r.gormClient.Updates(teamEntity) result := r.gormClient.Updates(teamEntity)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
r.CaptureLogs()
return nil return nil
} }
@@ -76,6 +133,9 @@ func (r *Repository) UpdateTeamStatus(teamEntity *TeamEntity) error {
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
r.CaptureLogs()
return nil return nil
} }

View File

@@ -55,3 +55,12 @@ func (r *Repository) IsAHoustonUser(nameOrSlackUserId string) (bool, *UserEntity
} }
return true, &existingUser return true, &existingUser
} }
func (r *Repository) GetHoustonUsersBySlackId(slackUserId []string) (*[]UserEntity, error) {
var users []UserEntity
result := r.gormClient.Where("slack_user_id IN ?", slackUserId).Find(&users)
if result.Error != nil {
return nil, result.Error
}
return &users, nil
}

View File

@@ -2,6 +2,7 @@ package service
import ( import (
"houston/model/incident" "houston/model/incident"
"houston/model/log"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
response "houston/service/response" response "houston/service/response"
@@ -60,8 +61,9 @@ func (f *filterService) GetFilters(c *gin.Context) {
func (f *filterService) GetEntityRepositories() ( func (f *filterService) GetEntityRepositories() (
*incident.Repository, *severity.Repository, *team.Repository) { *incident.Repository, *severity.Repository, *team.Repository) {
severityRepository := severity.NewSeverityRepository(f.logger, f.db) severityRepository := severity.NewSeverityRepository(f.logger, f.db)
incidentRepository := incident.NewIncidentRepository(f.logger, f.db, severityRepository) logRepository := log.NewLogRepository(f.logger, f.db)
teamRespository := team.NewTeamRepository(f.logger, f.db) teamRespository := team.NewTeamRepository(f.logger, f.db, logRepository)
incidentRepository := incident.NewIncidentRepository(f.logger, f.db, severityRepository, logRepository, teamRespository, nil)
return incidentRepository, severityRepository, teamRespository return incidentRepository, severityRepository, teamRespository
} }

View File

@@ -14,6 +14,7 @@ import (
"houston/common/util" "houston/common/util"
"houston/internal/processor/action/view" "houston/internal/processor/action/view"
"houston/model/incident" "houston/model/incident"
"houston/model/log"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
"houston/model/user" "houston/model/user"
@@ -36,11 +37,14 @@ type IncidentServiceV2 struct {
} }
func NewIncidentServiceV2(logger *zap.Logger, db *gorm.DB) *IncidentServiceV2 { func NewIncidentServiceV2(logger *zap.Logger, db *gorm.DB) *IncidentServiceV2 {
teamRepository := team.NewTeamRepository(logger, db) logRepository := log.NewLogRepository(logger, db)
teamRepository := team.NewTeamRepository(logger, db, logRepository)
severityRepository := severity.NewSeverityRepository(logger, db) severityRepository := severity.NewSeverityRepository(logger, db)
incidentRepository := incident.NewIncidentRepository(logger, db, severityRepository)
userRepository := user.NewUserRepository(logger, db) userRepository := user.NewUserRepository(logger, db)
slackService := slack.NewSlackService(logger) slackService := slack.NewSlackService(logger)
incidentRepository := incident.NewIncidentRepository(
logger, db, severityRepository, logRepository, teamRepository, slackService.SocketModeClient,
)
return &IncidentServiceV2{ return &IncidentServiceV2{
logger: logger, logger: logger,
db: db, db: db,

View File

@@ -8,6 +8,7 @@ import (
"houston/internal/processor/action" "houston/internal/processor/action"
"houston/internal/processor/action/view" "houston/internal/processor/action/view"
"houston/model/incident" "houston/model/incident"
"houston/model/log"
"houston/model/severity" "houston/model/severity"
"houston/model/team" "houston/model/team"
"houston/model/user" "houston/model/user"
@@ -45,9 +46,10 @@ type incidentService struct {
} }
func NewIncidentService(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, socketModeClient *socketmode.Client) *incidentService { func NewIncidentService(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, socketModeClient *socketmode.Client) *incidentService {
teamRepository := team.NewTeamRepository(logger, db)
severityRepository := severity.NewSeverityRepository(logger, db) severityRepository := severity.NewSeverityRepository(logger, db)
incidentRepository := incident.NewIncidentRepository(logger, db, severityRepository) logRepository := log.NewLogRepository(logger, db)
teamRepository := team.NewTeamRepository(logger, db, logRepository)
incidentRepository := incident.NewIncidentRepository(logger, db, severityRepository, logRepository, teamRepository, socketModeClient)
userRepository := user.NewUserRepository(logger, db) userRepository := user.NewUserRepository(logger, db)
messageUpdateAction := action.NewIncidentChannelMessageUpdateAction( messageUpdateAction := action.NewIncidentChannelMessageUpdateAction(
socketModeClient, logger, incidentRepository, teamRepository, severityRepository) socketModeClient, logger, incidentRepository, teamRepository, severityRepository)

60
service/log_service.go Normal file
View File

@@ -0,0 +1,60 @@
package service
import (
"errors"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
"houston/model/log"
service "houston/service/response"
common "houston/service/response/common"
"net/http"
"strconv"
)
type logService struct {
gin *gin.Engine
logger *zap.Logger
db *gorm.DB
logRepository *log.Repository
}
func NewLogService(gin *gin.Engine, logger *zap.Logger, db *gorm.DB) *logService {
logRepository := log.NewLogRepository(logger, db)
return &logService{
gin: gin,
logger: logger,
db: db,
logRepository: logRepository,
}
}
func (l *logService) GetLogs(c *gin.Context) {
logType := c.Param("log_type")
id := c.Param("id")
if len(logType) == 0 {
l.logger.Error("Log Type not provided")
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("Log type not provided"), http.StatusBadRequest, nil))
return
}
recordId, err := strconv.Atoi(id)
if err != nil {
l.logger.Error("error in converting string to int", zap.String("id", id), zap.Error(err))
c.JSON(http.StatusBadGateway, common.ErrorResponse(errors.New("Invalid record id"), http.StatusBadRequest, nil))
return
}
logs, err := l.logRepository.FetchLogsByRelationNameAndRecordId(logType, uint(recordId))
if err != nil {
l.logger.Error("error in fetching logs by relation name and record", zap.String("id", id), zap.String("relation_name", logType), zap.Error(err))
c.JSON(http.StatusBadGateway, common.ErrorResponse(errors.New("Error in fetching logs"), http.StatusBadRequest, nil))
return
}
logResponse := service.ConvertToLogResponse(logs, logType, uint(recordId))
c.JSON(http.StatusOK, common.SuccessResponse(logResponse, http.StatusOK))
}

View File

@@ -0,0 +1,38 @@
package service
import (
"gorm.io/datatypes"
"houston/model/log"
"time"
)
type LogResponse struct {
RelationName string `json:"relation_name"`
RecordId uint `json:"record_id"`
Logs []LogEntry `json:"logs"`
}
type LogEntry struct {
CreatedAt time.Time `json:"created_at"`
UserInfo datatypes.JSON `json:"user_info"`
Changes datatypes.JSON `json:"changes"`
}
func ConvertToLogResponse(logEnties []log.LogEntity, relationName string, recordId uint) LogResponse {
var logs []LogEntry
for index := range logEnties {
logEntry := LogEntry{
CreatedAt: logEnties[index].CreatedAt,
UserInfo: logEnties[index].UserInfo,
Changes: logEnties[index].Changes,
}
logs = append(logs, logEntry)
}
return LogResponse{
RelationName: relationName,
RecordId: recordId,
Logs: logs,
}
}

View File

@@ -1,8 +1,12 @@
package service package service
type ChannelMembersResponse struct {
Participants []UserResponse `json:"participants"`
Others []UserResponse `json:"others"`
}
type UserResponse struct { type UserResponse struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Image string `json:"image"` Image string `json:"image"`
} }

View File

@@ -10,6 +10,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
commonutil "houston/common/util" commonutil "houston/common/util"
"houston/model/log"
"houston/model/team" "houston/model/team"
"houston/pkg/slackbot" "houston/pkg/slackbot"
request "houston/service/request" request "houston/service/request"
@@ -40,7 +41,8 @@ func NewTeamService(gin *gin.Engine, logger *zap.Logger, db *gorm.DB, client *sl
} }
func (t *TeamService) AddTeam(c *gin.Context) { func (t *TeamService) AddTeam(c *gin.Context) {
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
minLength := viper.GetInt("TEAM_NAME_MIN_LENGTH") minLength := viper.GetInt("TEAM_NAME_MIN_LENGTH")
maxLength := viper.GetInt("TEAM_NAME_MAX_LENGTH") maxLength := viper.GetInt("TEAM_NAME_MAX_LENGTH")
authResult, _ := t.authService.checkIfManagerOrAdmin(c, "", Admin) authResult, _ := t.authService.checkIfManagerOrAdmin(c, "", Admin)
@@ -165,7 +167,8 @@ func isUserInvalid(userInfo *slack.User, err error) bool {
func (t *TeamService) GetTeams(c *gin.Context) { func (t *TeamService) GetTeams(c *gin.Context) {
teamId := c.Param("id") teamId := c.Param("id")
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
if teamId != "" { if teamId != "" {
TeamId, err := strconv.Atoi(teamId) TeamId, err := strconv.Atoi(teamId)
@@ -226,7 +229,8 @@ func (t *TeamService) GetTeams(c *gin.Context) {
} }
func (t *TeamService) UpdateTeam(c *gin.Context) { func (t *TeamService) UpdateTeam(c *gin.Context) {
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
userEmail := c.GetHeader("X-User-Email") userEmail := c.GetHeader("X-User-Email")
var updateTeamRequest request.UpdateTeamRequest var updateTeamRequest request.UpdateTeamRequest
@@ -333,6 +337,7 @@ func (t *TeamService) UpdateTeam(c *gin.Context) {
teamEntity.OncallHandle = (*slackUser)[0].ID teamEntity.OncallHandle = (*slackUser)[0].ID
} }
} }
teamEntity.UpdatedBy = userEmail
teamRepository.UpdateTeam(teamEntity) teamRepository.UpdateTeam(teamEntity)
c.JSON(http.StatusMultiStatus, common.SuccessResponse("Team updated successfully", http.StatusOK)) c.JSON(http.StatusMultiStatus, common.SuccessResponse("Team updated successfully", http.StatusOK))
@@ -343,7 +348,8 @@ func (t *TeamService) RemoveTeamMember(c *gin.Context) {
teamId := c.Param("id") teamId := c.Param("id")
slackUserId := c.Param("userId") slackUserId := c.Param("userId")
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId) TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil { if err != nil {
t.logger.Error(err.Error(), zap.String("teamId", teamId), zap.Error(err)) t.logger.Error(err.Error(), zap.String("teamId", teamId), zap.Error(err))
@@ -397,7 +403,8 @@ func (t *TeamService) MakeManager(c *gin.Context) {
teamId := c.Param("id") teamId := c.Param("id")
teamMemberToMakeManager := c.Param("userId") teamMemberToMakeManager := c.Param("userId")
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId) TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil { if err != nil {
@@ -450,7 +457,8 @@ func (t *TeamService) RemoveTeam(c *gin.Context) {
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is not an admin", userEmail)), http.StatusUnauthorized, nil)) c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is not an admin", userEmail)), http.StatusUnauthorized, nil))
return return
} }
teamRepository := team.NewTeamRepository(t.logger, t.db) logRepository := log.NewLogRepository(t.logger, t.db)
teamRepository := team.NewTeamRepository(t.logger, t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId) TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil { if err != nil {
t.logger.Error("error reading team id", zap.Error(err)) t.logger.Error("error reading team id", zap.Error(err))

View File

@@ -7,7 +7,12 @@ import (
"github.com/slack-go/slack/socketmode" "github.com/slack-go/slack/socketmode"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/gorm" "gorm.io/gorm"
util "houston/common/util"
"houston/internal/cron" "houston/internal/cron"
"houston/model/incident"
"houston/model/log"
"houston/model/severity"
"houston/model/team"
"houston/model/user" "houston/model/user"
"houston/pkg/slackbot" "houston/pkg/slackbot"
service "houston/service/response" service "houston/service/response"
@@ -15,27 +20,36 @@ import (
"net/http" "net/http"
) )
type userService struct { type UserService struct {
gin *gin.Engine gin *gin.Engine
logger *zap.Logger logger *zap.Logger
client *slackbot.Client client *slackbot.Client
db *gorm.DB db *gorm.DB
socketModeClient *socketmode.Client socketModeClient *socketmode.Client
authService *AuthService authService *AuthService
userRepository *user.Repository
incidentRepository *incident.Repository
teamRepository *team.Repository
} }
func NewUserService(gin *gin.Engine, logger *zap.Logger, client *slackbot.Client, db *gorm.DB, socketModeClient *socketmode.Client, authService *AuthService) *userService { func NewUserService(gin *gin.Engine, logger *zap.Logger, client *slackbot.Client, db *gorm.DB,
return &userService{ socketModeClient *socketmode.Client, authService *AuthService) *UserService {
gin: gin, logRepository := log.NewLogRepository(logger, db)
logger: logger, teamRepository := team.NewTeamRepository(logger, db, logRepository)
client: client, return &UserService{
db: db, gin: gin,
socketModeClient: socketModeClient, logger: logger,
authService: authService, client: client,
db: db,
socketModeClient: socketModeClient,
authService: authService,
userRepository: user.NewUserRepository(logger, db),
incidentRepository: incident.NewIncidentRepository(logger, db, severity.NewSeverityRepository(logger, db), logRepository, teamRepository, socketModeClient),
teamRepository: teamRepository,
} }
} }
func (u *userService) GetUserInfo(c *gin.Context) { func (u *UserService) GetUserInfo(c *gin.Context) {
userId := c.Param("id") userId := c.Param("id")
users, err := u.client.GetUsersInfo(userId) users, err := u.client.GetUsersInfo(userId)
@@ -53,37 +67,61 @@ func (u *userService) GetUserInfo(c *gin.Context) {
}, http.StatusOK)) }, http.StatusOK))
} }
func (u *userService) GetUsersInConversation(c *gin.Context) { func (u *UserService) GetUsersInConversation(c *gin.Context) {
channelId := c.Query("channel_id") channelId := c.Query("channel_id")
incidentEntity, err := u.incidentRepository.FindIncidentByChannelId(channelId)
if err != nil {
u.logger.Error("error in getting incident by channel id", zap.String("channelId", channelId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
if incidentEntity == nil {
err := errors.New(fmt.Sprintf("incident with channel id %v not found", channelId))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamEntity, err := u.teamRepository.FindTeamById(incidentEntity.TeamId)
if err != nil {
u.logger.Error(fmt.Sprintf("error getting team info for team with id %v", incidentEntity.TeamId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
users, err := u.client.GetUsersInConversation(channelId) users, err := u.client.GetUsersInConversation(channelId)
if err != nil { if err != nil {
u.logger.Error("error in getting users from conversation", zap.String("channelId", channelId), zap.Error(err)) u.logger.Error(fmt.Sprintf("error in getting users from channel %v", channelId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return return
} }
usersInfo, err := u.client.GetUsersInfo(users...) usersInfo, err := u.client.GetUsersInfo(users...)
if err != nil { if err != nil {
u.logger.Error("error in getting users info", zap.Any("userIds", users), zap.Error(err)) u.logger.Error("error in getting users info", zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return return
} }
usersData := *usersInfo usersData := *usersInfo
var usersResponses []service.UserResponse = []service.UserResponse{} teamMembers := util.ConvertSliceToMapOfString(teamEntity.SlackUserIds)
var participants []service.UserResponse
for userInfo := range usersData { var others []service.UserResponse
usersResponses = append(usersResponses, service.UserResponse{ for userIndex := range usersData {
Id: usersData[userInfo].ID, userInfo := service.UserResponse{
Name: usersData[userInfo].Profile.RealName, Id: usersData[userIndex].ID,
Email: usersData[userInfo].Profile.Email, Name: usersData[userIndex].Profile.RealName,
Image: usersData[userInfo].Profile.Image32, Email: usersData[userIndex].Profile.Email,
}) Image: usersData[userIndex].Profile.Image32,
}
if teamMembers[usersData[userIndex].ID] != "" {
participants = append(participants, userInfo)
} else {
others = append(others, userInfo)
}
} }
c.JSON(http.StatusOK, common.SuccessResponse(service.ChannelMembersResponse{
c.JSON(http.StatusOK, common.SuccessResponse(usersResponses, http.StatusOK)) Participants: participants,
Others: others,
}, http.StatusOK))
} }
func (u *userService) UpdateHoustonUsers(c *gin.Context) { func (u *UserService) UpdateHoustonUsers(c *gin.Context) {
userEmail := c.GetHeader("X-User-Email") userEmail := c.GetHeader("X-User-Email")
authResult, _ := u.authService.checkIfManagerOrAdmin(c, "", Admin, Manager) authResult, _ := u.authService.checkIfManagerOrAdmin(c, "", Admin, Manager)

View File

@@ -0,0 +1,40 @@
package service
import (
"fmt"
"reflect"
)
type Difference struct {
Attribute string `json:"attribute"`
From string `json:"from"`
To string `json:"to"`
}
func DeepCompare(before, after interface{}) []Difference {
var differences []Difference
valBefore := reflect.ValueOf(before)
valAfter := reflect.ValueOf(after)
if valBefore.Type() != valAfter.Type() {
return append(differences, Difference{"TypeMismatch", valBefore.Type().String(), valAfter.Type().String()})
}
for index := 0; index < valBefore.NumField(); index++ {
fieldType := valBefore.Type().Field(index)
fieldName := fieldType.Name
fieldBefore := valBefore.Field(index)
fieldAfter := valAfter.Field(index)
if fieldName != "Model" && !reflect.DeepEqual(fieldBefore.Interface(), fieldAfter.Interface()) {
differences = append(differences, Difference{
Attribute: fieldName,
From: fmt.Sprintf("%v", fieldBefore.Interface()),
To: fmt.Sprintf("%v", fieldAfter.Interface()),
})
}
}
return differences
}