Files
kopia/notification/notification_send.go
Jarek Kowalski fec575bd90 fix(server): fixed server-based notifications (#4598)
* fix(server): fixed server-based notifications

Used TypedEventArgs instead of `any` to ensure all notification data
carries type information, allowing the server to property deserialize it.

* fix
2025-05-24 08:15:45 -07:00

220 lines
6.4 KiB
Go

// Package notification provides a mechanism to send notifications for various events.
package notification
import (
"bytes"
"context"
"encoding/json"
stderrors "errors"
"os"
"time"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/internal/grpcapi"
"github.com/kopia/kopia/notification/notifydata"
"github.com/kopia/kopia/notification/notifyprofile"
"github.com/kopia/kopia/notification/notifytemplate"
"github.com/kopia/kopia/notification/sender"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/logging"
)
// AdditionalSenders is a list of additional senders that will be used in addition to the senders configured in the repository.
//
//nolint:gochecknoglobals
var AdditionalSenders []sender.Sender
var log = logging.Module("notification")
// TemplateArgs represents the arguments passed to the notification template when rendering.
type TemplateArgs struct {
Hostname string
EventTime time.Time
EventArgs any
EventArgsType grpcapi.NotificationEventArgType
KopiaRepo string
KopiaBuildInfo string
KopiaBuildVersion string
}
// Severity represents the severity of a notification message.
type Severity = sender.Severity
const (
// SeverityVerbose includes all notification messages, including frequent and verbose ones.
SeverityVerbose Severity = -100
// SeveritySuccess is used for successful operations.
SeveritySuccess Severity = -10
// SeverityDefault includes notification messages enabled by default.
SeverityDefault Severity = 0
// SeverityReport is used for periodic reports.
SeverityReport Severity = 0
// SeverityWarning is used for warnings about potential issues.
SeverityWarning Severity = 10
// SeverityError is used for errors that require attention.
SeverityError Severity = 20
)
// SeverityToNumber maps severity names to numbers.
//
//nolint:gochecknoglobals
var SeverityToNumber = map[string]Severity{
"verbose": SeverityVerbose,
"success": SeveritySuccess,
"report": SeverityReport,
"warning": SeverityWarning,
"error": SeverityError,
}
// SeverityToString maps severity numbers to names.
//
//nolint:gochecknoglobals
var SeverityToString map[Severity]string
func init() {
SeverityToString = make(map[Severity]string)
for k, v := range SeverityToNumber {
SeverityToString[v] = k
}
}
func notificationSendersFromRepo(ctx context.Context, rep repo.Repository, severity Severity) ([]sender.Sender, error) {
profiles, err := notifyprofile.ListProfiles(ctx, rep)
if err != nil {
return nil, errors.Wrap(err, "unable to list notification profiles")
}
var result []sender.Sender
for _, p := range profiles {
if severity < p.MinSeverity {
continue
}
s, err := sender.GetSender(ctx, p.ProfileName, p.MethodConfig.Type, p.MethodConfig.Config)
if err != nil {
log(ctx).Warnw("unable to create sender for notification profile", "profile", p.ProfileName, "err", err)
continue
}
result = append(result, s)
}
return result, nil
}
// Send sends a notification for the given event.
// Any errors encountered during the process are logged.
func Send(ctx context.Context, rep repo.Repository, templateName string, eventArgs notifydata.TypedEventArgs, sev Severity, opt notifytemplate.Options) {
// if we're connected to a repository server, send the notification there.
if rem, ok := rep.(repo.RemoteNotifications); ok {
jsonData, err := json.Marshal(eventArgs)
if err != nil {
log(ctx).Warnw("unable to marshal event args", "err", err)
return
}
if err := rem.SendNotification(ctx, templateName, jsonData, eventArgs.EventArgsType(), int32(sev)); err != nil {
log(ctx).Warnw("unable to send notification", "err", err)
}
return
}
if err := SendInternal(ctx, rep, templateName, eventArgs, sev, opt); err != nil {
log(ctx).Warnw("unable to send notification", "err", err)
}
}
// SendInternal sends a notification for the given event and returns an error.
func SendInternal(ctx context.Context, rep repo.Repository, templateName string, eventArgs notifydata.TypedEventArgs, sev Severity, opt notifytemplate.Options) error {
senders, err := notificationSendersFromRepo(ctx, rep, sev)
if err != nil {
return errors.Wrap(err, "unable to get notification senders")
}
senders = append(senders, AdditionalSenders...)
var resultErr error
for _, s := range senders {
if err := SendTo(ctx, rep, s, templateName, eventArgs, sev, opt); err != nil {
resultErr = stderrors.Join(resultErr, err)
}
}
return resultErr
}
// MakeTemplateArgs wraps event-specific arguments into TemplateArgs object.
func MakeTemplateArgs(eventArgs notifydata.TypedEventArgs) TemplateArgs {
now := clock.Now()
h, _ := os.Hostname()
if h == "" {
h = "unknown hostname"
}
// prepare template arguments
return TemplateArgs{
Hostname: h,
EventArgs: eventArgs,
EventTime: now,
KopiaRepo: repo.BuildGitHubRepo,
KopiaBuildInfo: repo.BuildInfo,
KopiaBuildVersion: repo.BuildVersion,
}
}
// SendTo sends a notification to the given sender.
func SendTo(ctx context.Context, rep repo.Repository, s sender.Sender, templateName string, eventArgs notifydata.TypedEventArgs, sev Severity, opt notifytemplate.Options) error {
// execute template
var bodyBuf bytes.Buffer
tmpl, err := notifytemplate.ResolveTemplate(ctx, rep, s.ProfileName(), templateName, s.Format())
if err != nil {
return errors.Wrap(err, "unable to resolve notification template")
}
t, err := notifytemplate.ParseTemplate(tmpl, opt)
if err != nil {
return errors.Wrap(err, "unable to parse notification template")
}
if err := t.Execute(&bodyBuf, MakeTemplateArgs(eventArgs)); err != nil {
return errors.Wrap(err, "unable to execute notification template")
}
// extract headers from the template
msg, err := sender.ParseMessage(ctx, &bodyBuf)
if err != nil {
return errors.Wrap(err, "unable to parse message from notification template")
}
msg.Severity = sev
var resultErr error
if err := s.Send(ctx, msg); err != nil {
resultErr = stderrors.Join(resultErr, errors.Wrap(err, "unable to send notification message"))
}
return resultErr
}
// SendTestNotification sends a test notification to the given sender.
func SendTestNotification(ctx context.Context, rep repo.Repository, s sender.Sender) error {
log(ctx).Infof("Sending test notification to %v", s.Summary())
return SendTo(ctx, rep, s, notifytemplate.TestNotification, notifydata.EmptyEventData{}, SeveritySuccess, notifytemplate.DefaultOptions)
}