Skip to content

Slack integration for alerts and reports - #4371

Merged
begelundmuller merged 34 commits into
mainfrom
4183-slack-integration
Apr 3, 2024
Merged

Slack integration for alerts and reports#4371
begelundmuller merged 34 commits into
mainfrom
4183-slack-integration

Conversation

@esevastyanov

@esevastyanov esevastyanov commented Mar 18, 2024

Copy link
Copy Markdown
Contributor
  • slack driver to deliver notifications to channels and application "messages" tab (by email)
  • slack driver to deliver notifications using webhooks
  • refactored alert yaml/options/spec
  • refactored report yaml/options/spec
  • backward compatibility for alert yaml
  • backward compatibility for report yaml
  • docs
  • slack application
  • metrics
  • unit tests
  • polish message templates

alert spec has a notify section

...
notify:
  on_recover: true
  renotify: true
  renotify_after: 24h
  email:
    recipients:
      - recipient@example.com
      ...
  slack:
    emails:
      - recipient@example.com
      ...
    channels:
      - alerts
      ...
    webhooks:
      - https://...
      ...

report spec has notify section

notify:
  email:
    recipients:
      - recipient@example.com
      ...
  slack:
    emails:
      - recipient@example.com
      ...
    channels:
      - alerts
      ...
    webhooks:
      - https://...
      ...

slack driver only needs a bot token to post messages to channels and "messages" tab

start devproject --env connector.slack.bot_token=xoxb-...

Required scopes for a Slack application

Permission Description
chat:write Send messages
users:read View people in a workspace (required only to search a user by an email)
users:read.email View email addresses of people in a workspace (required only to search a user by an email)
incoming-webhook Post messages to specific channels in Slack

@esevastyanov
esevastyanov force-pushed the 4183-slack-integration branch from c305231 to 3fa8b65 Compare March 18, 2024 19:40
@esevastyanov
esevastyanov force-pushed the 4183-slack-integration branch from 3fa8b65 to c1d3303 Compare March 18, 2024 19:41
@esevastyanov
esevastyanov force-pushed the 4183-slack-integration branch from e1b463c to 00c3859 Compare March 18, 2024 22:00
@esevastyanov
esevastyanov marked this pull request as ready for review March 19, 2024 09:36
@esevastyanov esevastyanov changed the title Slack integration for alerts Slack integration for alerts and reports Mar 19, 2024
@esevastyanov

Copy link
Copy Markdown
Contributor Author

@AdityaHegde could you please review the change of UI components? The only thing is done there - I renamed api parameters related to email recipients of alerts and reports

Comment thread runtime/compilers/rillv1/parse_alert.go Outdated
}

// Validate recipients
// Validate email recipients

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives an impression that someone can use both older format as well as newer flow but we really should allow one format.

Comment thread runtime/drivers/slack/slack.go Outdated
return h, true
}

func (h *handle) SendScheduledReport(s *drivers.ScheduledReport, r drivers.RecipientOpts) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend moving notifier related functions to notifier.go file in this pkg or similar.

Comment thread runtime/reconcilers/alert.go Outdated
}

if a.Spec.SlackChannels != nil || a.Spec.SlackEmails != nil {
conn, release, err := r.C.Runtime.AcquireHandle(ctx, r.C.InstanceID, "slack")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be hardcoded but should come from alert spec to support more connectors in future.

Comment thread runtime/reconcilers/alert.go Outdated
}
}

if a.Spec.SlackChannels != nil || a.Spec.SlackEmails != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also have webhooks. Should also use len(a.Spec.SlackChannels) > 0 instead of nil checks.

Comment thread proto/rill/runtime/v1/resources.proto Outdated
ExportFormat export_format = 8;
repeated string email_recipients = 9;
map<string, string> annotations = 10;
repeated string slack_channels = 10;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec should just have a NotifierConnector and NotifierProperties instead of separate slack related fields to support different notifiers in future. Refer to parse_api.go and usage of Resolver and ResolverProperties in in APISpec for similar constructs.

Comment thread runtime/reconcilers/report.go Outdated
}

if rep.Spec.SlackChannels != nil || rep.Spec.SlackEmails != nil {
conn, release, err := r.C.Runtime.AcquireHandle(ctx, r.C.InstanceID, "slack")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as in alert.go i.e. connector should be available from the spec.

Comment thread admin/server/reports.go Outdated
}

if len(opts.Recipients) == 0 {
if len(opts.EmailRecipients) == 0 && len(opts.SlackEmails) == 0 && len(opts.SlackChannels) == 0 && len(opts.SlackWebhooks) == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally there should be a deduping of SlackWebhooks too similar to emails.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the ability to unsubscribe is a requirement for private notifications. As for group notifications (slack webhooks, channels), there is a way to update the corresponding subscription via EditReport

@AdityaHegde AdityaHegde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from a UI perspective

Comment thread proto/rill/runtime/v1/resources.proto
Comment thread runtime/drivers/slack/notifier.go Outdated
Comment thread runtime/drivers/slack/notifier.go Outdated
Comment thread admin/server/alerts.go Outdated
Comment on lines +556 to +560
opts.EmailRecipients = pbutil.ToSliceString(props["recipients"].([]any))
case "slack":
opts.SlackUsers = pbutil.ToSliceString(props[slack.UsersField].([]any))
opts.SlackChannels = pbutil.ToSliceString(props[slack.ChannelsField].([]any))
opts.SlackWebhooks = pbutil.ToSliceString(props[slack.WebhooksField].([]any))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These type casts might be a little fragile. If one of them are nil, the .([]any) will panic. Probably best to have a small util function that returns nil if the cast fails (i.e. val, ok := props[key].([]any))

Comment thread admin/server/reports.go Outdated
Comment on lines +551 to +556
case "email":
opts.EmailRecipients = pbutil.ToSliceString(props["recipients"].([]any))
case "slack":
opts.SlackUsers = pbutil.ToSliceString(props[slack.UsersField].([]any))
opts.SlackChannels = pbutil.ToSliceString(props[slack.ChannelsField].([]any))
opts.SlackWebhooks = pbutil.ToSliceString(props[slack.WebhooksField].([]any))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment

Comment on lines 15 to 17
type AlertStatus struct {
ToEmail string
ToName string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove these fields from AlertStatus since they won't be available for channels and webhooks? (The actual email to send to will be available in the notifier props for email delivery.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove this once email notifier is created analogously to Slack notifier. Placed a todo reminder

Comment thread runtime/drivers/slack/notifier.go Outdated
Comment on lines +34 to +37
func newNotifier(token string, props map[string]any) *notifier {
users := pbutil.ToSliceString(props[UsersField].([]any))
channels := pbutil.ToSliceString(props[ChannelsField].([]any))
webhooks := pbutil.ToSliceString(props[WebhooksField].([]any))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cleaner to decode into a struct here using the mapstructure package (like we do for connector, source and resolver properties). It handles type conversion automatically and makes it easier to see which properties are supported by the notifier. For example:

type notifier {
  token string
  props *notifierProperties
  templates *template.Template
}

type notifierProperties {
  Users []string `mapstructure:"users"`
  Channels []string `mapstructure:"channels"`
  Webhooks []string `mapstructure:"webhooks"`
}

func newNotifier(token string, propsMap map[string]any) *notifier {
  props := &notifierProperties{}
  err := mapstructure.WeakDecode(propsMap, props)
  if err != nil {
    return nil, err
  }
  ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved Slack props encode/decode into Slack notifier so that it is possible to reuse them

Comment thread runtime/reconcilers/report.go Outdated
for _, notifier := range rep.Spec.Notifiers {
switch notifier.Connector {
case "email":
recipients := pbutil.ToSliceString(notifier.Properties.AsMap()["recipients"].([]any))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also suggest a val, ok := ... type assertion here to keep it safe from panics

Comment on lines +400 to +408
switch notifier.Connector {
case "email":
recipients := pbutil.ToSliceString(notifier.Properties.AsMap()["recipients"].([]any))
for _, recipient := range recipients {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about safe assertions

Comment on lines 448 to 456

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment thread runtime/drivers/drivers.go Outdated
Comment on lines +142 to +144
// AsNotifier returns a Notifier (if the driver can serve as such) to send notifications: alerts, reports, etc.
// Examples: email notifier, slack notifier.
AsNotifier(properties *structpb.Struct) (Notifier, error)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass a map type here (call .AsMap() upstream) – best to deserialize protos early to use native types as much as possible

Comment thread runtime/drivers/slack/notifier.go Outdated
Comment on lines +196 to +204
func EncodeProps(users, channels, webhooks []string) (*structpb.Struct, error) {
return structpb.NewStruct(map[string]any{
"users": pbutil.ToSliceAny(users),
"channels": pbutil.ToSliceAny(channels),
"webhooks": pbutil.ToSliceAny(webhooks),
})
}

func DecodeProps(propsStruct *structpb.Struct) (*NotifierProperties, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same idea – best to use native types like map internally, and serialize/deserialze to protos only when needed (i.e. in places that read/write proto types).

@begelundmuller

Copy link
Copy Markdown
Contributor

Also note the merge conflicts

Comment thread runtime/reconcilers/alert.go Outdated
specHashTrigger := a.State.SpecHash != specHash
refsTrigger := a.State.RefsHash != refsHash && a.Spec.RefreshSchedule != nil && a.Spec.RefreshSchedule.RefUpdate
scheduleTrigger := a.State.NextRunOn != nil && !a.State.NextRunOn.AsTime().After(time.Now())
scheduleTrigger := a.State.NextRunOn != nil && a.State.NextRunOn.AsTime().Before(time.Now())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not equivalent – specifically, if NextRunOn == time.Now(), we should trigger

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted and fixed linter changes

@begelundmuller
begelundmuller removed the request for review from k-anshul April 3, 2024 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants