Skip to content

Commit 06d51b5

Browse files
committed
drtprod: add Slack notification support and YAML progress tracking
This PR builds the capability in the YAML processor to send slack updates if the slack token and a slack channel is passed as a flag. This enables us to provide status of YAML execution which becomes handy when we are running scale tests. Currently, we manually put an update on the channel with the status. To enable notification, we need to pass the slack-token and slack-channel flags and also, ensure that "notify_progress" flag is set to true for all targets and steps that requires slack notification. Epic: None Release note: None
1 parent eb447f5 commit 06d51b5

File tree

5 files changed

+366
-89
lines changed

5 files changed

+366
-89
lines changed

pkg/cmd/drtprod/cli/commands/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go_library(
44
name = "commands",
55
srcs = [
66
"rootcmd.go",
7+
"slack.go",
78
"yamlprocessor.go",
89
],
910
importpath = "github.com/cockroachdb/cockroach/pkg/cmd/drtprod/cli/commands",
@@ -21,6 +22,7 @@ go_library(
2122
"@com_github_alessio_shellescape//:shellescape",
2223
"@com_github_cockroachdb_errors//:errors",
2324
"@com_github_datadog_datadog_api_client_go_v2//api/datadogV1",
25+
"@com_github_slack_go_slack//:slack",
2426
"@com_github_spf13_cobra//:cobra",
2527
"@in_gopkg_yaml_v2//:yaml_v2",
2628
"@org_golang_x_exp//maps",
@@ -36,6 +38,7 @@ go_test(
3638
"//pkg/roachprod/install",
3739
"//pkg/roachprod/logger",
3840
"//pkg/util/syncutil",
41+
"@com_github_cockroachdb_errors//:errors",
3942
"@com_github_stretchr_testify//require",
4043
],
4144
)

pkg/cmd/drtprod/cli/commands/slack.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright 2024 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package commands
7+
8+
import (
9+
"fmt"
10+
11+
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
12+
"github.com/slack-go/slack"
13+
)
14+
15+
type Status string
16+
17+
const (
18+
StatusStarting Status = "Starting"
19+
StatusCompleted Status = "Completed"
20+
StatusFailed Status = "Failed"
21+
)
22+
23+
// Notifier is an interface for sending notifications for target and step status updates
24+
type Notifier interface {
25+
// SendTargetNotification sends a notification to Slack when a target starts or finishes
26+
SendTargetNotification(targetName string, status Status) error
27+
// SendStepNotification sends a notification to Slack when a step starts or finishes
28+
SendStepNotification(targetName, command string, status Status) error
29+
}
30+
31+
// SlackNotifier implements the Notifier interface for Slack
32+
type SlackNotifier struct {
33+
token string // Slack bot token
34+
channel string // Slack channel to post messages to
35+
enabled bool // Whether Slack integration is enabled
36+
slackClient *slack.Client
37+
// Map to store slack thread timestamps for each target combination
38+
// This is useful to group messages for the same target in a thread
39+
threadTimestamps map[string]string
40+
threadTimestampsLock syncutil.Mutex
41+
}
42+
43+
// NewSlackNotifier creates a new SlackNotifier
44+
func NewSlackNotifier(botToken, channel string) Notifier {
45+
sn := &SlackNotifier{
46+
token: botToken,
47+
channel: channel,
48+
threadTimestamps: make(map[string]string),
49+
}
50+
sn.initSlackIntegration()
51+
return sn
52+
}
53+
54+
// InitSlackIntegration initializes the Slack integration
55+
func (sn *SlackNotifier) initSlackIntegration() {
56+
// Check if Slack integration is enabled
57+
if sn.token == "" || sn.channel == "" {
58+
return
59+
}
60+
61+
// Create Slack client
62+
sn.slackClient = slack.New(sn.token)
63+
sn.enabled = true
64+
fmt.Println("Slack integration initialized successfully")
65+
}
66+
67+
// SendStepNotification sends a notification to Slack when a step starts or finishes
68+
func (sn *SlackNotifier) SendStepNotification(targetName, command string, status Status) error {
69+
if !sn.enabled || sn.slackClient == nil {
70+
return nil
71+
}
72+
73+
var blocks []slack.Block
74+
75+
blocks = append(blocks, slack.NewSectionBlock(
76+
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("%v Command: `%s`", status, command), false, false),
77+
nil,
78+
nil,
79+
))
80+
81+
// Check if we have a thread timestamp for this workflow+target
82+
threadTS := sn.getThreadTimestamp(targetName)
83+
84+
var options []slack.MsgOption
85+
options = append(options, slack.MsgOptionBlocks(blocks...))
86+
87+
// If we have a thread timestamp, add it to the options
88+
if threadTS != "" {
89+
options = append(options, slack.MsgOptionTS(threadTS))
90+
}
91+
92+
// Send the message to Slack
93+
_, timestamp, err := sn.slackClient.PostMessage(
94+
sn.channel,
95+
options...,
96+
)
97+
98+
// If this is the first message for this workflow+target, store the timestamp
99+
if threadTS == "" && err == nil {
100+
sn.setThreadTimestamp(targetName, timestamp)
101+
}
102+
103+
return err
104+
}
105+
106+
// SendTargetNotification sends a notification to Slack when a step starts or finishes
107+
func (sn *SlackNotifier) SendTargetNotification(targetName string, status Status) error {
108+
if !sn.enabled || sn.slackClient == nil {
109+
return nil
110+
}
111+
messageText := fmt.Sprintf("%v Target *%s*.", status, targetName)
112+
113+
// Create buttons for continue, retry (if failure), and abort
114+
var blocks []slack.Block
115+
blocks = append(blocks, slack.NewSectionBlock(
116+
slack.NewTextBlockObject("mrkdwn", messageText, false, false),
117+
nil,
118+
nil,
119+
))
120+
// Check if we have a thread timestamp for this workflow+target
121+
threadTS := sn.getThreadTimestamp(targetName)
122+
123+
var options []slack.MsgOption
124+
options = append(options, slack.MsgOptionBlocks(blocks...))
125+
126+
// If we have a thread timestamp, add it to the options
127+
if threadTS != "" {
128+
options = append(options, slack.MsgOptionTS(threadTS))
129+
}
130+
131+
// Send the message to Slack
132+
_, timestamp, err := sn.slackClient.PostMessage(
133+
sn.channel,
134+
options...,
135+
)
136+
137+
// If this is the first message for this workflow+target, store the timestamp
138+
if threadTS == "" && err == nil {
139+
sn.setThreadTimestamp(targetName, timestamp)
140+
}
141+
142+
return err
143+
}
144+
145+
// getThreadTimestamp returns the thread timestamp for a workflow+target combination
146+
// If no thread timestamp exists, it returns an empty string
147+
func (sn *SlackNotifier) getThreadTimestamp(targetName string) string {
148+
sn.threadTimestampsLock.Lock()
149+
defer sn.threadTimestampsLock.Unlock()
150+
return sn.threadTimestamps[targetName]
151+
}
152+
153+
// setThreadTimestamp sets the thread timestamp for a workflow+target combination
154+
func (sn *SlackNotifier) setThreadTimestamp(targetName, timestamp string) {
155+
sn.threadTimestampsLock.Lock()
156+
defer sn.threadTimestampsLock.Unlock()
157+
sn.threadTimestamps[targetName] = timestamp
158+
}

0 commit comments

Comments
 (0)