Ctrl K
ComunidadGuías…

notifications/slack.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package notifications

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/coroot/coroot/db"
	"github.com/coroot/coroot/model"
	"github.com/coroot/coroot/utils"
	"github.com/slack-go/slack"
)

type Slack struct {
	channel string
	client  *slack.Client
}

func NewSlack(token, channel string) *Slack {
	return &Slack{
		channel: channel,
		client:  slack.New(token),
	}
}

func (s *Slack) SendIncident(ctx context.Context, baseUrl string, n *db.IncidentNotification) error {
	var ch, ts string
	parts := strings.Split(n.ExternalKey, ":")
	if len(parts) == 2 {
		ch, ts = parts[0], parts[1]
	}
	if ch == "" {
		ch = s.channel
	}
	var header, snippet string
	if n.Status == model.OK {
		header = fmt.Sprintf("<%s|*%s* incident resolved>", incidentUrl(baseUrl, n), n.ApplicationId.Name)
		snippet = n.ApplicationId.Name + " incident resolved"
	} else {
		header = fmt.Sprintf("[%s] <%s|*%s* is not meeting its SLOs>", strings.ToUpper(n.Status.String()), incidentUrl(baseUrl, n), n.ApplicationId.Name)
		snippet = n.ApplicationId.Name + " is not meeting its SLOs"
	}
	var details []string
	if n.Details != nil {
		for _, r := range n.Details.Reports {
			details = append(details, fmt.Sprintf("• *%s* / %s: %s", r.Name, r.Check, r.Message))
		}
	}
	blocks := []slack.Block{
		s.section(s.text("%s", header)),
	}
	if len(details) > 0 {
		blocks = append(blocks, s.section(s.text("%s", strings.Join(details, "\n"))))
	}
	if n.Details != nil && n.Details.RCASummary != "" {
		rca := "*Root Cause*: " + n.Details.RCASummary
		if n.Details.RCARemediations != "" {
			rca += "\n*Remediations*: " + utils.Truncate(n.Details.RCARemediations, 2000)
		}
		blocks = append(blocks, s.section(s.text("%s", rca)))
	}
	body := s.body(n.Status.Color(), snippet, blocks...)
	opts := []slack.MsgOption{body, slack.MsgOptionDisableLinkUnfurl()}
	if ts != "" {
		opts = append(opts, slack.MsgOptionTS(ts), slack.MsgOptionBroadcast())
	}
	var err error
	ch, ts, err = s.client.PostMessageContext(ctx, ch, opts...)
	if err != nil {
		return fmt.Errorf("slack error: %w", err)
	}
	n.ExternalKey = fmt.Sprintf("%s:%s", ch, ts)
	return nil
}

func (s *Slack) SendDeployment(ctx context.Context, project *db.Project, ds model.ApplicationDeploymentStatus) error {
	d := ds.Deployment

	status := "Deployed"
	switch ds.State {
	case model.ApplicationDeploymentStateInProgress:
		status = "In-progress"
	case model.ApplicationDeploymentStateStuck:
		status = "Stuck"
	case model.ApplicationDeploymentStateCancelled:
		status = "Cancelled"
	}

	var summary *slack.SectionBlock
	if ds.State == model.ApplicationDeploymentStateSummary {
		items := "No notable changes"
		if len(ds.Summary) > 0 {
			items = ""
			var sb strings.Builder
			for _, s := range ds.Summary {
				fmt.Fprintf(&sb, "%s %s\n", s.Emoji(), s.Message)
			}
			items += sb.String()
		}
		summary = s.section(s.text("*Summary*\n%s", items))
	}
	url := deploymentUrl(project.Settings.Integrations.BaseUrl, project.Id, d)
	blocks := []slack.Block{
		s.section(s.text("Deployment of <%s|*%s*> to *%s*", url, d.ApplicationId.Name, project.Name)),
		s.section(nil,
			s.text("*Status*\n%s", status),
			s.text("*Version*\n<%s|*%s*>", url, d.Version()),
		),
	}
	if summary != nil {
		blocks = append(blocks, summary)
	}
	blocks = append(blocks, slack.NewContextBlock("", s.text("<!date^%d^{date_short_pretty} at {time}|%s>", d.StartedAt, d.StartedAt.ToStandard().Format(time.RFC1123))))

	snippet := fmt.Sprintf("Deployment of %s to %s", d.ApplicationId.Name, project.Name)
	channel := s.channel
	options := []slack.MsgOption{s.body(ds.Status.Color(), snippet, blocks...)}
	if d.Notifications.Slack.Channel != "" && d.Notifications.Slack.ThreadTs != "" {
		channel = d.Notifications.Slack.Channel
		options = append(options, slack.MsgOptionUpdate(d.Notifications.Slack.ThreadTs))
	}
	ch, ts, _, err := s.client.SendMessageContext(ctx, channel, options...)
	if err != nil {
		return fmt.Errorf("slack error: %w", err)
	}
	d.Notifications.Slack.Channel = ch
	d.Notifications.Slack.ThreadTs = ts

	if ds.State == model.ApplicationDeploymentStateSummary {
		status = "Summary"
	}
	snippet += ": " + status
	reply := s.body(ds.Status.Color(), snippet, s.section(s.text("%s", ds.Message)))
	if summary != nil {
		reply = slack.MsgOptionBlocks(summary)
	}
	_, _, _, err = s.client.SendMessageContext(ctx, ch, slack.MsgOptionPost(), slack.MsgOptionTS(ts), reply)
	if err != nil {
		return fmt.Errorf("slack error: %w", err)
	}

	return nil
}

func (s *Slack) SendAlert(ctx context.Context, baseUrl string, n *db.AlertNotification) error {
	var ch, ts string
	parts := strings.Split(n.ExternalKey, ":")
	if len(parts) == 2 {
		ch, ts = parts[0], parts[1]
	}
	if ch == "" {
		ch = s.channel
	}
	displayName := alertDisplayName(n)
	var header, snippet string
	if n.Status == model.OK {
		resolvedText := "resolved"
		if n.Details != nil && n.Details.ResolvedBy != "" {
			resolvedText = fmt.Sprintf("manually resolved by *%s*", n.Details.ResolvedBy)
		}
		if n.Details != nil && n.Details.Duration != "" {
			header = fmt.Sprintf("<%s|*%s* alert %s> (duration: %s)", alertUrl(baseUrl, n), displayName, resolvedText, n.Details.Duration)
			snippet = fmt.Sprintf("%s alert %s (duration: %s)", displayName, resolvedText, n.Details.Duration)
		} else {
			header = fmt.Sprintf("<%s|*%s* alert %s>", alertUrl(baseUrl, n), displayName, resolvedText)
			snippet = fmt.Sprintf("%s alert %s", displayName, resolvedText)
		}
	} else {
		header = fmt.Sprintf("[%s] <%s|*%s*: %s>", strings.ToUpper(n.Status.String()), alertUrl(baseUrl, n), displayName, n.Details.Summary)
		snippet = fmt.Sprintf("%s: %s", displayName, n.Details.Summary)
	}
	var details []string
	if n.Details != nil {
		if n.Details.ProjectName != "" {
			details = append(details, "*Project*: "+n.Details.ProjectName)
		}
		if n.Details.RuleName != "" {
			details = append(details, "*Alerting rule*: "+n.Details.RuleName)
		}
		for _, d := range n.Details.Details {
			if d.Code {
				details = append(details, fmt.Sprintf("*%s*:\n```%s```", d.Name, d.Value))
			} else {
				details = append(details, fmt.Sprintf("*%s*: %s", d.Name, d.Value))
			}
		}
	}
	blocks := []slack.Block{
		s.section(s.text("%s", header)),
	}
	if len(details) > 0 {
		blocks = append(blocks, s.section(s.text("%s", strings.Join(details, "\n"))))
	}
	body := s.body(n.Status.Color(), snippet, blocks...)
	opts := []slack.MsgOption{body, slack.MsgOptionDisableLinkUnfurl()}
	if ts != "" {
		opts = append(opts, slack.MsgOptionTS(ts), slack.MsgOptionBroadcast())
	}
	var err error
	ch, ts, err = s.client.PostMessageContext(ctx, ch, opts...)
	if err != nil {
		return fmt.Errorf("slack error: %w", err)
	}
	n.ExternalKey = fmt.Sprintf("%s:%s", ch, ts)
	return nil
}

func (s *Slack) body(color string, fallback string, blocks ...slack.Block) slack.MsgOption {
	return slack.MsgOptionAttachments(slack.Attachment{
		Color:    color,
		Blocks:   slack.Blocks{BlockSet: blocks},
		Fallback: fallback,
	})
}

func (s *Slack) section(text *slack.TextBlockObject, fields ...*slack.TextBlockObject) *slack.SectionBlock {
	return slack.NewSectionBlock(text, fields, nil)
}

func (s *Slack) text(format string, a ...any) *slack.TextBlockObject {
	text := format
	if len(a) > 0 {
		text = fmt.Sprintf(format, a...)
	}
	return slack.NewTextBlockObject(slack.MarkdownType, text, false, false)
}