Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: default title and body for notifications #535

Merged
merged 16 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions api/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ type ContextUser struct {

const UserIDHeaderKey = "X-User-ID"

var SystemUserID *uuid.UUID
var CanaryCheckerPath string
var ApmHubPath string
var Kubernetes kubernetes.Interface
var Namespace string
var (
SystemUserID *uuid.UUID
CanaryCheckerPath string
ApmHubPath string
Kubernetes kubernetes.Interface
Namespace string

// Full URL of the mission control web UI.
PublicWebURL string
)

// type alias because the name "Context" collides with gocontext
// and embedding both wouldn't have been possible.
Expand Down
16 changes: 8 additions & 8 deletions api/responders.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ type IncidentResponders struct {
}

type Responder struct {
ID uuid.UUID `json:"id,omitempty"`
Type string `json:"type"`
Properties types.JSONStringMap `json:"properties" gorm:"type:jsonstringmap;<-:false"`
ExternalID string `json:"external_id,omitempty"`
IncidentID uuid.UUID `json:"incident_id,omitempty"`
Incident Incident `json:"incident,omitempty"`
TeamID uuid.UUID `json:"team_id,omitempty"`
Team Team `json:"team,omitempty"`
ID uuid.UUID `json:"id,omitempty"`
Type string `json:"type"`
Properties types.JSONMap `json:"properties" gorm:"<-:false"`
ExternalID string `json:"external_id,omitempty"`
IncidentID uuid.UUID `json:"incident_id,omitempty"`
Incident Incident `json:"incident,omitempty"`
TeamID uuid.UUID `json:"team_id,omitempty"`
Team Team `json:"team,omitempty"`
}

type NotificationSpec struct {
Expand Down
3 changes: 1 addition & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ var Root = &cobra.Command{

var dev bool
var httpPort, metricsPort, devGuiPort int
var publicEndpoint = "http://localhost:8080"
var configDb, authMode, kratosAPI, kratosAdminAPI, postgrestURI string
var clerkJWKSURL, clerkOrgID string
var disablePostgrest bool
Expand All @@ -48,7 +47,7 @@ func ServerFlags(flags *pflag.FlagSet) {
flags.IntVar(&devGuiPort, "devGuiPort", 3004, "Port used by a local npm server in development mode")
flags.IntVar(&metricsPort, "metricsPort", 8081, "Port to expose a health dashboard ")
flags.BoolVar(&dev, "dev", false, "Run in development mode")
flags.StringVar(&publicEndpoint, "public-endpoint", "http://localhost:8080", "Public endpoint that this instance is exposed under")
adityathebe marked this conversation as resolved.
Show resolved Hide resolved
flags.StringVar(&api.PublicWebURL, "public-endpoint", "http://localhost:3000", "Public endpoint this instance is exposed under")
flags.StringVar(&api.ApmHubPath, "apm-hub", "http://apm-hub:8080", "APM Hub URL")
flags.StringVar(&configDb, "config-db", "http://config-db:8080", "Config DB URL")
flags.StringVar(&kratosAPI, "kratos-api", "http://kratos-public:80", "Kratos API service")
Expand Down
2 changes: 1 addition & 1 deletion cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ var Serve = &cobra.Command{
PreRun: PreRun,
Run: func(cmd *cobra.Command, args []string) {
// PostgREST needs to know how it is exposed to create the correct links
db.HttpEndpoint = publicEndpoint + "/db"
db.HttpEndpoint = api.PublicWebURL + "/db"
if authMode != "" {
db.PostgresDBAnonRole = "postgrest_api"
}
Expand Down
108 changes: 101 additions & 7 deletions events/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"

"github.com/flanksource/commons/template"
"github.com/flanksource/commons/utils"
"github.com/flanksource/duty"
"github.com/flanksource/duty/models"
"github.com/flanksource/incident-commander/api"
pkgNotification "github.com/flanksource/incident-commander/notification"
Expand Down Expand Up @@ -47,20 +49,21 @@ func NewNotificationSaveConsumerSync() SyncEventConsumer {
func NewNotificationSendConsumerAsync() AsyncEventConsumer {
return AsyncEventConsumer{
watchEvents: []string{EventNotificationSend},
consumer: processNotificationEvents,
consumer: sendNotifications,
batchSize: 1,
numConsumers: 5,
}
}

func processNotificationEvents(ctx *api.Context, events []api.Event) []api.Event {
func sendNotifications(ctx *api.Context, events []api.Event) []api.Event {
var failedEvents []api.Event
for _, e := range events {
if err := sendNotification(ctx, e); err != nil {
e.Error = err.Error()
failedEvents = append(failedEvents, e)
}
}

return failedEvents
}

Expand All @@ -76,6 +79,7 @@ type NotificationEventProperties struct {
// NotificationTemplate holds in data for notification
// that'll be used by struct templater.
type NotificationTemplate struct {
Title string `template:"true"`
Message string `template:"true"`
Properties map[string]string `template:"true"`
}
Expand All @@ -92,6 +96,62 @@ func (t *NotificationEventProperties) FromMap(m map[string]string) {
_ = json.Unmarshal(b, &t)
}

// labelsTemplate is a helper func to generate the template for displaying labels
func labelsTemplate(field string) string {
return fmt.Sprintf("{{if %s}}### Labels: \n{{range $k, $v := %s}}**{{$k}}**: {{$v}} \n{{end}}{{end}}", field, field)
}

// defaultTitleAndBody returns the default title and body for notification
// based on the given event.
func defaultTitleAndBody(event string) (title string, body string) {
switch event {
case EventCheckPassed:
title = "Check {{.check.name}} has passed"
body = fmt.Sprintf("%s\nCanary: {{.canary.name}} \nAgent: {{.agent.name}}\n\n[Reference]({{.permalink}})", labelsTemplate(".check.labels"))

case EventCheckFailed:
title = "Check {{.check.name}} has failed"
body = fmt.Sprintf("%s\nCanary: {{.canary.name}} \nAgent: {{.agent.name}}\n\n[Reference]({{.permalink}})", labelsTemplate(".check.labels"))

case EventComponentStatusHealthy, EventComponentStatusUnhealthy, EventComponentStatusInfo, EventComponentStatusWarning, EventComponentStatusError:
title = "Component {{.component.name}} status updated to {{.component.status}}"
body = fmt.Sprintf("%s\n[Reference]({{.permalink}})", labelsTemplate(".component.labels"))

case EventIncidentCommentAdded:
title = "{{.author.name}} left a comment on {{.incident.incident_id}}: {{.incident.title}}"
body = "{{.comment.comment}}\n\n[Reference]({{.permalink}})"

case EventIncidentCreated:
title = "{{.incident.incident_id}}: {{.incident.title}} ({{.incident.severity}}) created"
body = "Type: {{.incident.type}}\n\n[Reference]({{.permalink}})"

case EventIncidentDODAdded:
title = "Definition of Done added | {{.incident.incident_id}}: {{.incident.title}}"
body = "Evidence: {{.evidence.description}}\n\n[Reference]({{.permalink}})"

case EventIncidentDODPassed, EventIncidentDODRegressed:
title = "Definition of Done {{if .evidence.done}}passed{{else}}regressed{{end}} | {{.incident.incident_id}}: {{.incident.title}}"
body = "Evidence: {{.evidence.description}}\nHypothesis: {{.hypothesis.title}}\n\n[Reference]({{.permalink}})"

case EventIncidentResponderAdded:
title = "New responder added to {{.incident.incident_id}}: {{.incident.title}}"
body = "Responder {{.responder.name}}\n\n[Reference]({{.permalink}})"

case EventIncidentResponderRemoved:
title = "Responder removed from {{.incident.incident_id}}: {{.incident.title}}"
body = "Responder {{.responder.name}}\n\n[Reference]({{.permalink}})"

case EventIncidentStatusCancelled, EventIncidentStatusClosed, EventIncidentStatusInvestigating, EventIncidentStatusMitigated, EventIncidentStatusOpen, EventIncidentStatusResolved:
title = "{{.incident.title}} status updated"
body = "New Status: {{.incident.status}}\n\n[Reference]({{.permalink}})"

case EventTeamUpdate, EventTeamDelete, EventNotificationUpdate, EventNotificationDelete, EventPlaybookSpecApprovalUpdated, EventPlaybookApprovalInserted:
// Not applicable
}

return title, body
}

func sendNotification(ctx *api.Context, event api.Event) error {
var props NotificationEventProperties
props.FromMap(event.Properties)
Expand All @@ -115,8 +175,11 @@ func sendNotification(ctx *api.Context, event api.Event) error {
return err
}

defaultTitle, defaultBody := defaultTitleAndBody(props.EventName)

data := NotificationTemplate{
Message: notification.Template,
Title: utils.Coalesce(notification.Title, defaultTitle),
Message: utils.Coalesce(notification.Template, defaultBody),
Properties: notification.Properties,
}

Expand All @@ -131,7 +194,7 @@ func sendNotification(ctx *api.Context, event api.Event) error {
}

smtpURL := fmt.Sprintf("%s?ToAddresses=%s", pkgNotification.SystemSMTP, url.QueryEscape(emailAddress))
return pkgNotification.Send(ctx, "", smtpURL, notification.Title, data.Message, data.Properties)
return pkgNotification.Send(ctx, "", smtpURL, data.Title, data.Message, data.Properties)
}

if props.TeamID != "" {
Expand All @@ -149,7 +212,7 @@ func sendNotification(ctx *api.Context, event api.Event) error {
return fmt.Errorf("error templating notification: %w", err)
}

return pkgNotification.Send(ctx, cn.Connection, cn.URL, notification.Title, data.Message, data.Properties, cn.Properties)
return pkgNotification.Send(ctx, cn.Connection, cn.URL, data.Title, data.Message, data.Properties, cn.Properties)
}
}

Expand All @@ -162,7 +225,7 @@ func sendNotification(ctx *api.Context, event api.Event) error {
return fmt.Errorf("error templating notification: %w", err)
}

return pkgNotification.Send(ctx, cn.Connection, cn.URL, notification.Title, data.Message, data.Properties, cn.Properties)
return pkgNotification.Send(ctx, cn.Connection, cn.URL, data.Title, data.Message, data.Properties, cn.Properties)
}

return nil
Expand Down Expand Up @@ -192,7 +255,7 @@ func addNotificationEvent(ctx *api.Context, event api.Event) error {
return err
}

if !n.HasRecipients() || n.Template == "" {
if !n.HasRecipients() {
continue
}

Expand Down Expand Up @@ -295,6 +358,8 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st
env := make(map[string]any)

if strings.HasPrefix(eventName, "check.") {
checkID := properties["id"]

var check models.Check
if err := ctx.DB().Where("id = ?", properties["id"]).Find(&check).Error; err != nil {
return nil, err
Expand All @@ -305,8 +370,23 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st
return nil, err
}

summary, err := duty.CheckSummary(ctx, checkID)
if err != nil {
return nil, err
} else if summary != nil {
check.Uptime = summary.Uptime
check.Latency = summary.Latency
}

var agent models.Agent
if err := ctx.DB().Where("id = ?", check.AgentID).First(&agent).Error; err != nil {
adityathebe marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}

env["canary"] = canary.AsMap()
env["check"] = check.AsMap()
env["agent"] = agent.AsMap()
env["permalink"] = fmt.Sprintf("%s/health?layout=table&checkId=%s&timeRange=1h", api.PublicWebURL, check.ID)
}

if eventName == "incident.created" || strings.HasPrefix(eventName, "incident.status.") {
Expand All @@ -316,6 +396,7 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st
}

env["incident"] = incident.AsMap()
env["permalink"] = fmt.Sprintf("%s/incidents/%s", api.PublicWebURL, incident.ID)
}

if strings.HasPrefix(eventName, "incident.responder.") {
Expand All @@ -331,6 +412,7 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st

env["incident"] = incident.AsMap()
env["responder"] = responder.AsMap()
env["permalink"] = fmt.Sprintf("%s/incidents/%s", api.PublicWebURL, incident.ID)
}

if strings.HasPrefix(eventName, "incident.comment.") {
Expand All @@ -344,10 +426,21 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st
return nil, err
}

var author models.Person
if err := ctx.DB().Where("id = ?", comment.CreatedBy).Find(&author).Error; err != nil {
return nil, err
}

// TODO: extract out mentioned users' emails from the comment body

env["incident"] = incident.AsMap()
env["comment"] = comment.AsMap()
env["author"] = map[string]string{
"id": author.ID.String(),
"name": author.Name,
"email": author.Email,
}
env["permalink"] = fmt.Sprintf("%s/incidents/%s", api.PublicWebURL, incident.ID)
}

if strings.HasPrefix(eventName, "incident.dod.") {
Expand All @@ -369,6 +462,7 @@ func getEnvForEvent(ctx *api.Context, eventName string, properties map[string]st
env["evidence"] = evidence.AsMap()
env["hypotheses"] = hypotheses.AsMap()
env["incident"] = incident.AsMap()
env["permalink"] = fmt.Sprintf("%s/incidents/%s", api.PublicWebURL, incident.ID)
}

return env, nil
Expand Down
Loading
Loading