blob: a659a327e581fd5402109194eee3e67c1dc92f40 [file] [log] [blame]
// Package notify is a package for sending notifications.
package notify
import (
"context"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/perf/go/alerts"
ag "go.skia.org/infra/perf/go/anomalygroup/notifier"
"go.skia.org/infra/perf/go/clustering2"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/git/provider"
"go.skia.org/infra/perf/go/notify/common"
"go.skia.org/infra/perf/go/notifytypes"
"go.skia.org/infra/perf/go/stepfit"
"go.skia.org/infra/perf/go/ui/frame"
)
// Formatter has implementations for both HTML and Markdown.
type Formatter interface {
// Return body and subject.
FormatNewRegression(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, URL string, frame *frame.FrameResponse) (string, string, error)
FormatRegressionMissing(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, URL string, frame *frame.FrameResponse) (string, string, error)
}
// Transport has implementations for email, issuetracker, and the noop implementation.
type Transport interface {
SendNewRegression(ctx context.Context, alert *alerts.Alert, body, subject string) (threadingReference string, err error)
SendRegressionMissing(ctx context.Context, threadingReference string, alert *alerts.Alert, body, subject string) (err error)
UpdateRegressionNotification(ctx context.Context, alert *alerts.Alert, body, notificationId string) (err error)
}
const (
fromAddress = "alertserver@skia.org"
)
// TemplateContext is used in expanding the message templates.
type TemplateContext struct {
// URL is the root URL of the Perf instance.
URL string
// ViewOnDashboard is the URL to view the regressing traces on the explore
// page.
ViewOnDashboard string
// PreviousCommit is the previous commit the regression was found at.
//
// All commits that might be blamed for causing the regression
// are in the range `(PreviousCommit, Commit]`, that is inclusive of
// Commit but exclusive of PreviousCommit.
PreviousCommit provider.Commit
// Commit is the commit the regression was found at.
Commit provider.Commit
// CommitURL is a URL that points to the above Commit. The value of this URL
// can be controlled via the `--commit_range_url` flag.
CommitURL string
// Alert is the configuration for the alert that found the regression.
Alert *alerts.Alert
// Cluster is all the information found about the regression.
Cluster *clustering2.ClusterSummary
// ParamSet for all the matching traces.
ParamSet paramtools.ReadOnlyParamSet
}
// Notifier provides an interface for regression notification functions
type Notifier interface {
// RegressionFound sends a notification for the given cluster found at the given commit.
RegressionFound(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, regressionID string) (string, error)
// RegressionMissing sends a notification that a previous regression found for
// the given cluster found at the given commit has disappeared after more data
// has arrived.
RegressionMissing(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, threadingReference string) error
// ExampleSend sends an example for dummy data for the given alerts.Config.
ExampleSend(ctx context.Context, alert *alerts.Alert) error
UpdateNotification(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, notificationId string) error
}
// defaultNotifier sends notifications.
type defaultNotifier struct {
notificationDataProvider NotificationDataProvider
formatter Formatter
transport Transport
// url is the URL of this instance of Perf.
url string
}
// newNotifier returns a newNotifier Notifier.
func newNotifier(notificationDataProvider NotificationDataProvider, formatter Formatter, transport Transport, url string) Notifier {
return &defaultNotifier{
notificationDataProvider: notificationDataProvider,
formatter: formatter,
transport: transport,
url: url,
}
}
// RegressionFound sends a notification for the given cluster found at the given commit. Where to send it is defined in the alerts.Config.
func (n *defaultNotifier) RegressionFound(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, regressionID string) (string, error) {
notificationData, err := n.notificationDataProvider.GetNotificationDataRegressionFound(ctx, common.RegressionMetadata{
CurrentCommit: commit,
PreviousCommit: previousCommit,
AlertConfig: alert,
Cl: cl,
Frame: frame,
InstanceUrl: n.url,
})
if err != nil {
return "", err
}
threadingReference, err := n.transport.SendNewRegression(ctx, alert, notificationData.Body, notificationData.Subject)
if err != nil {
return "", skerr.Wrapf(err, "sending new regression message")
}
return threadingReference, nil
}
// RegressionMissing sends a notification that a previous regression found for
// the given cluster found at the given commit has disappeared after more data
// has arrived. Where to send it is defined in the alerts.Config.
func (n *defaultNotifier) RegressionMissing(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, threadingReference string) error {
notificationData, err := n.notificationDataProvider.GetNotificationDataRegressionMissing(ctx, common.RegressionMetadata{
CurrentCommit: commit,
PreviousCommit: previousCommit,
AlertConfig: alert,
Cl: cl,
Frame: frame,
InstanceUrl: n.url,
})
if err != nil {
return err
}
if err := n.transport.SendRegressionMissing(ctx, threadingReference, alert, notificationData.Body, notificationData.Subject); err != nil {
return skerr.Wrapf(err, "sending regression missing message")
}
return nil
}
// ExampleSend sends an example for dummy data for the given alerts.Config.
func (n *defaultNotifier) ExampleSend(ctx context.Context, alert *alerts.Alert) error {
commit := provider.Commit{
Subject: "An example commit use for testing.",
URL: "https://skia.googlesource.com/skia/+show/d261e1075a93677442fdf7fe72aba7e583863664",
GitHash: "d261e1075a93677442fdf7fe72aba7e583863664",
Timestamp: 1498176000,
}
previousCommit := provider.Commit{
Subject: "An example previous commit to use for testing.",
URL: "https://skia.googlesource.com/skia/+/fb49909acafba5e031b90a265a6ce059cda85019",
GitHash: "fb49909acafba5e031b90a265a6ce059cda85019",
Timestamp: 1687824470,
}
cl := &clustering2.ClusterSummary{
Num: 10,
StepFit: &stepfit.StepFit{
Status: stepfit.HIGH,
},
StepPoint: &dataframe.ColumnHeader{
Offset: 2,
Timestamp: 1498176000,
},
}
frame := &frame.FrameResponse{
DataFrame: &dataframe.DataFrame{
Header: []*dataframe.ColumnHeader{
{Offset: 1, Timestamp: 1687824470},
{Offset: 2, Timestamp: 1498176000},
},
ParamSet: paramtools.ReadOnlyParamSet{
"device_name": []string{"sailfish", "sargo", "wembley"},
},
},
}
threadingReference, err := n.RegressionFound(ctx, commit, previousCommit, alert, cl, frame, "")
if err != nil {
return skerr.Wrap(err)
}
err = n.RegressionMissing(ctx, commit, previousCommit, alert, cl, frame, threadingReference)
if err != nil {
return skerr.Wrap(err)
}
return nil
}
func (n *defaultNotifier) UpdateNotification(ctx context.Context, commit, previousCommit provider.Commit, alert *alerts.Alert, cl *clustering2.ClusterSummary, frame *frame.FrameResponse, notificationId string) error {
body, _, err := n.formatter.FormatNewRegression(ctx, commit, previousCommit, alert, cl, n.url, frame)
if err != nil {
return err
}
return n.transport.UpdateRegressionNotification(ctx, alert, body, notificationId)
}
// New returns a Notifier of the selected type.
func New(ctx context.Context, cfg *config.NotifyConfig, URL, commitRangeURITemplate string) (Notifier, error) {
formatter, err := getFormatter(cfg, commitRangeURITemplate)
if err != nil {
return nil, skerr.Wrap(err)
}
var notificationDataProvider NotificationDataProvider
switch cfg.NotificationDataProvider {
case "":
notificationDataProvider = newDefaultNotificationProvider(formatter)
}
switch cfg.Notifications {
case notifytypes.None:
return newNotifier(notificationDataProvider, formatter, NewNoopTransport(), URL), nil
case notifytypes.HTMLEmail:
return newNotifier(notificationDataProvider, formatter, NewEmailTransport(), URL), nil
case notifytypes.MarkdownIssueTracker:
tracker, err := NewIssueTrackerTransport(ctx, cfg)
if err != nil {
return nil, skerr.Wrap(err)
}
return newNotifier(notificationDataProvider, formatter, tracker, URL), nil
case notifytypes.ChromeperfAlerting:
return NewChromePerfNotifier(ctx, nil)
case notifytypes.AnomalyGrouper:
return ag.NewAnomalyGroupNotifier(ctx, nil), nil
default:
return nil, skerr.Fmt("invalid Notifier type: %s, must be one of: %v", cfg.Notifications, notifytypes.AllNotifierTypes)
}
}
// getFormatter returns a new Formatter instance.
func getFormatter(notifyConfig *config.NotifyConfig, commitRangeURITemplate string) (Formatter, error) {
switch notifyConfig.Notifications {
case notifytypes.None:
case notifytypes.HTMLEmail:
return NewHTMLFormatter(commitRangeURITemplate), nil
case notifytypes.MarkdownIssueTracker:
return NewMarkdownFormatter(commitRangeURITemplate, notifyConfig)
}
return nil, nil
}