pkg/degradation-detector/slack.go (212 lines of code) (raw):
package degradation_detector
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/slack-go/slack"
)
type SlackMessage struct {
Text string `json:"text"`
Channel string `json:"channel"`
}
type slackSettings interface {
CreateSlackMessage(degradations Degradation) SlackMessage
SlackChannel() string
ChartLink(timeRangeProvider TimeRangeProvider) string
}
type SlackSettings struct {
Channel string
// ProductLink is the part of the link on your dashboards after https://ij-perf.labs.jb.gg/.
// For example: intellij,ijent, kmt, clion
ProductLink string
}
func (s SlackSettings) SlackChannel() string {
return s.Channel
}
func eventLink(tests string, build string, timestamp int64) string {
projects := strings.Split(tests, ",")
escapedProjects := make([]string, 0, len(projects))
for _, p := range projects {
escapedProjects = append(escapedProjects, url.QueryEscape(p))
}
date := time.UnixMilli(timestamp).UTC().Format("02-01-2006")
project := strings.Join(escapedProjects, ",")
return fmt.Sprintf("<https://ij-perf.labs.jb.gg/degradations/report?tests=%s&build=%s&date=%s|Report event>", project, build, date)
}
func (s PerformanceSettings) CreateSlackMessage(d Degradation) SlackMessage {
reason := getMessageBasedOnMedianChange(d.medianValues)
date := time.UnixMilli(d.timestamp).UTC().Format("02-01-2006 15:04:05")
link := s.ChartLink(d)
tests := strings.ReplaceAll(s.Project, ",", "\n")
mode := "default"
if s.Mode != "" {
mode = s.Mode
}
text := fmt.Sprintf(
"%sTest(s): %s\n"+
"Metric: %s\n"+
"Mode: %s\n"+
"Build: %s\n"+
"Branch: %s\n"+
"Date: %s\n"+
"Reason: %s\n"+
"%s\n"+
"%s", icon(d.medianValues), tests, s.Metric, mode, d.Build, s.Branch, date, reason, link, eventLink(s.Project, d.Build, d.timestamp))
return SlackMessage{
Text: text,
Channel: s.Channel,
}
}
func (s StartupSettings) CreateSlackMessage(d Degradation) SlackMessage {
reason := getMessageBasedOnMedianChange(d.medianValues)
date := time.UnixMilli(d.timestamp).UTC().Format("02-01-2006 15:04:05")
link := s.ChartLink(d)
tests := strings.ReplaceAll(s.Project, ",", "\n")
text := fmt.Sprintf(
"%sProject(s): %s\n"+
"Metric: %s\n"+
"Build: %s\n"+
"Branch: %s\n"+
"Date: %s\n"+
"Reason: %s\n"+
"%s\n"+
"%s", icon(d.medianValues), tests, s.Metric, d.Build, s.Branch, date, reason, link, eventLink(s.Project, d.Build, d.timestamp))
return SlackMessage{
Text: text,
Channel: s.Channel,
}
}
func (s FleetStartupSettings) CreateSlackMessage(d Degradation) SlackMessage {
reason := getMessageBasedOnMedianChange(d.medianValues)
date := time.UnixMilli(d.timestamp).UTC().Format("02-01-2006 15:04:05")
link := s.ChartLink(d)
text := fmt.Sprintf(
"%sMetric: %s\n"+
"Build: %s\n"+
"Branch: %s\n"+
"Date: %s\n"+
"Reason: %s\n"+
"%s\n", icon(d.medianValues), s.Metric, d.Build, s.Branch, date, reason, link)
return SlackMessage{
Text: text,
Channel: s.Channel,
}
}
func (s FleetStartupSettings) ChartLink(d TimeRangeProvider) string {
machineGroup := getMachineGroup(s.Machine)
measurements := strings.Split(s.Metric, ",")
escapedMeasurements := make([]string, 0, len(measurements))
for _, m := range measurements {
escapedMeasurements = append(escapedMeasurements, url.QueryEscape(m))
}
measure := strings.Join(escapedMeasurements, "&measure=")
return fmt.Sprintf("<https://ij-perf.labs.jb.gg/fleet/startupExplore?machine=%s&branch=%s&measure=%s&%s|See charts>",
url.QueryEscape(machineGroup), url.QueryEscape(s.Branch), measure, getCustomRange(d.GetRangeStartTime(), time.Now()))
}
func getCustomRange(start, end time.Time) string {
currentDate := fmt.Sprintf("%d-%02d-%d", end.Year(), end.Month(), end.Day())
startDate := fmt.Sprintf("%d-%02d-%d", start.Year(), start.Month(), start.Day())
return fmt.Sprintf("timeRange=custom&customRange=%s:%s", startDate, currentDate)
}
type TimeRangeProvider interface {
GetRangeStartTime() time.Time
}
func (d Degradation) GetRangeStartTime() time.Time {
t := time.Unix(d.timestamp/1000, 0)
return t.AddDate(0, -1, 0)
}
func (s PerformanceSettings) ChartLink(d TimeRangeProvider) string {
build := ""
if d, ok := d.(Degradation); ok {
build = d.Build
}
testPage := "tests"
if strings.HasSuffix(s.Db, "Dev") {
testPage = "testsDev"
}
machineGroup := getMachineGroup(s.Machine)
projects := strings.Split(s.Project, ",")
measurements := strings.Split(s.Metric, ",")
escapedProjects := make([]string, 0, len(projects))
escapedMeasurements := make([]string, 0, len(measurements))
for _, p := range projects {
escapedProjects = append(escapedProjects, url.QueryEscape(p))
}
for _, m := range measurements {
escapedMeasurements = append(escapedMeasurements, url.QueryEscape(m))
}
mode := "default"
if s.Mode != "" {
mode = s.Mode
}
project := strings.Join(escapedProjects, "&project=")
measure := strings.Join(escapedMeasurements, "&measure=")
return fmt.Sprintf("<https://ij-perf.labs.jb.gg/%s/%s?mode=%s&machine=%s&branch=%s&project=%s&measure=%s&%s&point=%s|See charts>",
s.ProductLink, testPage, mode, url.QueryEscape(machineGroup), url.QueryEscape(s.Branch), project, measure, getCustomRange(d.GetRangeStartTime(), time.Now()), build)
}
func (s StartupSettings) ChartLink(d TimeRangeProvider) string {
machineGroup := getMachineGroup(s.Machine)
return fmt.Sprintf("<https://ij-perf.labs.jb.gg/ij/explore?machine=%s&branch=%s&product=%s&project=%s&%s|See charts>",
url.QueryEscape(machineGroup), url.QueryEscape(s.Branch), url.QueryEscape(s.Product), url.QueryEscape(s.Project), getCustomRange(d.GetRangeStartTime(), time.Now()))
}
func SendDegradationsToSlack(insertionResults <-chan DegradationWithSettings, client *http.Client) {
var wg sync.WaitGroup
for result := range insertionResults {
wg.Go(func() {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
message := result.Settings.CreateSlackMessage(result.Details)
err := SendSlackMessage(ctx, client, message)
if err != nil {
slog.Error("error while sending slack message", "error", err, "message", message)
return
}
slog.Info("slack message was sent", "degradation", message)
})
}
wg.Wait()
}
func SendSlackMessage(ctx context.Context, client *http.Client, slackMessage SlackMessage) error {
slackToken := os.Getenv("SLACK_TOKEN")
if slackToken == "" {
return errors.New("SLACK_TOKEN is not set")
}
api := slack.New(slackToken, slack.OptionHTTPClient(client))
_, _, _, err := api.SendMessageContext(ctx, slackMessage.Channel, slack.MsgOptionText(slackMessage.Text, false))
return err
}
func getMachineGroup(pattern string) string {
machineGroupMap := map[string]string{
"intellij-linux-performance-aws-%": "Linux EC2 C6id.8xlarge (32 vCPU Xeon, 64 GB)",
"intellij-windows-performance-%": "Windows EC2 C6id.4xlarge or i4i.4xlarge (16 vCPU Xeon, 32 or 128 GB)",
"intellij-linux-hw-hetzner%": "linux-blade-hetzner",
"intellij-linux-%-hetzner-%": "linux-blade-hetzner",
"intellij-linux-hw-munit-%": "Linux Munich i7-3770, 32 Gb",
"intellij-windows-hw-munit-%": "Windows Munich i7-3770, 32 Gb",
"intellij-macos-perf-eqx-%": "Mac Mini M2 Pro (10 vCPU, 32 GB)",
"intellij-macos-hw-munit-%": "macMini M1, 16 Gb",
}
return machineGroupMap[pattern]
}
func getMessageBasedOnMedianChange(medianValues CenterValues) string {
percentageChange := medianValues.PercentageChange()
medianMessage := fmt.Sprintf("Median changed by: %.2f%%. Median was %.2f and now it is %.2f.", percentageChange, medianValues.previousValue, medianValues.newValue)
if medianValues.newValue > medianValues.previousValue {
return "Degradation detected. " + medianMessage
}
return "Improvement detected. " + medianMessage
}
func icon(v CenterValues) string {
var icon string
if v.newValue > v.previousValue {
icon = ":chart_with_upwards_trend:"
} else {
icon = ":chart_with_downwards_trend:"
}
return icon
}