Skip to content

Commit c5d43db

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 aeaa401 commit c5d43db

File tree

5 files changed

+370
-89
lines changed

5 files changed

+370
-89
lines changed

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

Lines changed: 2 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",
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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+
"sync"
11+
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 sync.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+
return
66+
}
67+
68+
// SendStepNotification sends a notification to Slack when a step starts or finishes
69+
func (sn *SlackNotifier) SendStepNotification(targetName, command string, status Status) error {
70+
if !sn.enabled || sn.slackClient == nil {
71+
return nil
72+
}
73+
74+
var blocks []slack.Block
75+
76+
blocks = append(blocks, slack.NewSectionBlock(
77+
slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("%v Command: `%s`", status, command), false, false),
78+
nil,
79+
nil,
80+
))
81+
82+
// Check if we have a thread timestamp for this workflow+target
83+
threadTS := sn.getThreadTimestamp(targetName)
84+
85+
var options []slack.MsgOption
86+
options = append(options, slack.MsgOptionBlocks(blocks...))
87+
88+
// If we have a thread timestamp, add it to the options
89+
if threadTS != "" {
90+
options = append(options, slack.MsgOptionTS(threadTS))
91+
}
92+
93+
// Send the message to Slack
94+
_, timestamp, err := sn.slackClient.PostMessage(
95+
sn.channel,
96+
options...,
97+
)
98+
99+
// If this is the first message for this workflow+target, store the timestamp
100+
if threadTS == "" && err == nil {
101+
sn.setThreadTimestamp(targetName, timestamp)
102+
}
103+
104+
return err
105+
}
106+
107+
// SendTargetNotification sends a notification to Slack when a step starts or finishes
108+
func (sn *SlackNotifier) SendTargetNotification(targetName string, status Status) error {
109+
if !sn.enabled || sn.slackClient == nil {
110+
return nil
111+
}
112+
messageText := fmt.Sprintf("%v Target *%s*.", status, targetName)
113+
114+
// Create buttons for continue, retry (if failure), and abort
115+
var blocks []slack.Block
116+
blocks = append(blocks, slack.NewSectionBlock(
117+
slack.NewTextBlockObject("mrkdwn", messageText, false, false),
118+
nil,
119+
nil,
120+
))
121+
// Check if we have a thread timestamp for this workflow+target
122+
threadTS := sn.getThreadTimestamp(targetName)
123+
124+
var options []slack.MsgOption
125+
options = append(options, slack.MsgOptionBlocks(blocks...))
126+
127+
// If we have a thread timestamp, add it to the options
128+
if threadTS != "" {
129+
options = append(options, slack.MsgOptionTS(threadTS))
130+
}
131+
132+
// Send the message to Slack
133+
_, timestamp, err := sn.slackClient.PostMessage(
134+
sn.channel,
135+
options...,
136+
)
137+
138+
// If this is the first message for this workflow+target, store the timestamp
139+
if threadTS == "" && err == nil {
140+
sn.setThreadTimestamp(targetName, timestamp)
141+
}
142+
143+
return err
144+
}
145+
146+
// getThreadTimestamp returns the thread timestamp for a workflow+target combination
147+
// If no thread timestamp exists, it returns an empty string
148+
func (sn *SlackNotifier) getThreadTimestamp(targetName string) string {
149+
sn.threadTimestampsLock.Lock()
150+
defer sn.threadTimestampsLock.Unlock()
151+
152+
key := fmt.Sprintf("%s", targetName)
153+
return sn.threadTimestamps[key]
154+
}
155+
156+
// setThreadTimestamp sets the thread timestamp for a workflow+target combination
157+
func (sn *SlackNotifier) setThreadTimestamp(targetName, timestamp string) {
158+
sn.threadTimestampsLock.Lock()
159+
defer sn.threadTimestampsLock.Unlock()
160+
161+
key := fmt.Sprintf("%s", targetName)
162+
sn.threadTimestamps[key] = timestamp
163+
}

0 commit comments

Comments
 (0)