Skip to content

Commit ec1038c

Browse files
craig[bot]nameisbhaskar
andcommitted
Merge #153444
153444: drtprod: add Slack notification support and YAML progress tracking r=shailendra-patel,herkolategan a=nameisbhaskar 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 Co-authored-by: Bhaskarjyoti Bora <bhaskar.bora@cockroachlabs.com>
2 parents ce103d7 + 3e70695 commit ec1038c

File tree

5 files changed

+392
-130
lines changed

5 files changed

+392
-130
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
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2025 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+
"os"
11+
12+
"github.com/cockroachdb/cockroach/pkg/roachprod/config"
13+
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
14+
"github.com/slack-go/slack"
15+
)
16+
17+
type Status string
18+
19+
const (
20+
StatusStarting Status = "Starting"
21+
StatusCompleted Status = "Completed"
22+
StatusFailed Status = "Failed"
23+
24+
envSlackToken = "SLACK_BOT_TOKEN"
25+
envSlackChannel = "SLACK_CHANNEL"
26+
)
27+
28+
// Notifier is an interface for sending notifications for target and step status updates
29+
type Notifier interface {
30+
// SendNotification sends a notification to the defined endpoint.
31+
SendNotification(targetName string, message string) error
32+
}
33+
34+
// SlackNotifier implements the Notifier interface for Slack
35+
type SlackNotifier struct {
36+
channel string // Slack channel to post messages to
37+
enabled bool // Whether Slack integration is enabled
38+
slackClient *slack.Client
39+
// Maps each target to its Slack thread timestamp (`threadTS`), ensuring all
40+
// messages for the same target are posted in a single Slack thread.
41+
threadTimestamps map[string]string
42+
threadTimestampsLock syncutil.Mutex
43+
}
44+
45+
// NewSlackNotifier creates a new SlackNotifier
46+
func NewSlackNotifier() Notifier {
47+
sn := &SlackNotifier{
48+
threadTimestamps: make(map[string]string),
49+
}
50+
sn.initSlackIntegration(os.Getenv(envSlackToken), os.Getenv(envSlackChannel))
51+
return sn
52+
}
53+
54+
// InitSlackIntegration initializes the Slack integration
55+
func (sn *SlackNotifier) initSlackIntegration(botToken, channel string) {
56+
// Check if Slack integration is enabled
57+
if botToken == "" || channel == "" {
58+
return
59+
}
60+
61+
// Create the Slack client
62+
sn.slackClient = slack.New(botToken)
63+
sn.channel = channel
64+
sn.enabled = true
65+
config.Logger.Printf("Slack integration initialized successfully for slack channel '%s'\n", channel)
66+
}
67+
68+
// SendNotification sends a notification to the defined Slack endpoint.
69+
func (sn *SlackNotifier) SendNotification(targetName, message string) error {
70+
if !sn.enabled || sn.slackClient == nil {
71+
return nil
72+
}
73+
return sn.postMessage(targetName, message)
74+
}
75+
76+
// postMessage sends a message to Slack with the given blocks, handling thread tracking
77+
func (sn *SlackNotifier) postMessage(targetName string, messageText string) error {
78+
// Check if we have a thread timestamp for this target
79+
threadTS := sn.getThreadTimestamp(targetName)
80+
81+
var options []slack.MsgOption
82+
blocks := []slack.Block{
83+
slack.NewSectionBlock(
84+
slack.NewTextBlockObject("mrkdwn", messageText, false, false),
85+
nil,
86+
nil,
87+
),
88+
}
89+
90+
options = append(options, slack.MsgOptionBlocks(blocks...))
91+
92+
// `threadTS` is the timestamp of the parent message.
93+
// Including `threadTS` makes the new message a reply to that parent.
94+
if threadTS != "" {
95+
options = append(options, slack.MsgOptionTS(threadTS))
96+
}
97+
98+
// Send the message to Slack
99+
_, timestamp, err := sn.slackClient.PostMessage(
100+
sn.channel,
101+
options...,
102+
)
103+
104+
// If this is the first message for this target, store the timestamp
105+
if threadTS == "" && err == nil {
106+
sn.setThreadTimestamp(targetName, timestamp)
107+
}
108+
109+
return err
110+
}
111+
112+
// getThreadTimestamp returns the thread timestamp for a workflow+target combination
113+
// If no thread timestamp exists, it returns an empty string
114+
func (sn *SlackNotifier) getThreadTimestamp(targetName string) string {
115+
sn.threadTimestampsLock.Lock()
116+
defer sn.threadTimestampsLock.Unlock()
117+
return sn.threadTimestamps[targetName]
118+
}
119+
120+
// setThreadTimestamp sets the thread timestamp for a workflow+target combination
121+
func (sn *SlackNotifier) setThreadTimestamp(targetName, timestamp string) {
122+
sn.threadTimestampsLock.Lock()
123+
defer sn.threadTimestampsLock.Unlock()
124+
sn.threadTimestamps[targetName] = timestamp
125+
}
126+
127+
func buildTargetUpdateMessage(targetName string, status Status) string {
128+
return fmt.Sprintf("%v Target *%s*.", status, targetName)
129+
}
130+
131+
func buildStepUpdateMessage(command string, status Status) string {
132+
return fmt.Sprintf("%v Command: `%s`", status, command)
133+
}

0 commit comments

Comments
 (0)