The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/notifier/channels/telegram.go

222 lines
6.2 KiB

package channels
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/notifications"
)
var (
TelegramAPIURL = "https://api.telegram.org/bot%s/%s"
)
// Telegram supports 4096 chars max - from https://limits.tginfo.me/en.
const telegramMaxMessageLenRunes = 4096
// TelegramNotifier is responsible for sending
// alert notifications to Telegram.
type TelegramNotifier struct {
*Base
log log.Logger
images ImageStore
ns notifications.WebhookSender
tmpl *template.Template
settings telegramSettings
}
type telegramSettings struct {
BotToken string `json:"bottoken,omitempty" yaml:"bottoken,omitempty"`
ChatID string `json:"chatid,omitempty" yaml:"chatid,omitempty"`
Message string `json:"message,omitempty" yaml:"message,omitempty"`
}
func buildTelegramSettings(fc FactoryConfig) (telegramSettings, error) {
settings := telegramSettings{}
err := fc.Config.unmarshalSettings(&settings)
if err != nil {
return settings, fmt.Errorf("failed to unmarshal settings: %w", err)
}
settings.BotToken = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "bottoken", settings.BotToken)
if settings.BotToken == "" {
return settings, errors.New("could not find Bot Token in settings")
}
if settings.ChatID == "" {
return settings, errors.New("could not find Chat Id in settings")
}
if settings.Message == "" {
settings.Message = DefaultMessageEmbed
}
return settings, nil
}
func TelegramFactory(fc FactoryConfig) (NotificationChannel, error) {
notifier, err := NewTelegramNotifier(fc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
return notifier, nil
}
// NewTelegramNotifier is the constructor for the Telegram notifier
func NewTelegramNotifier(fc FactoryConfig) (*TelegramNotifier, error) {
settings, err := buildTelegramSettings(fc)
if err != nil {
return nil, err
}
return &TelegramNotifier{
Base: NewBase(&models.AlertNotification{
Uid: fc.Config.UID,
Name: fc.Config.Name,
Type: fc.Config.Type,
DisableResolveMessage: fc.Config.DisableResolveMessage,
Settings: fc.Config.Settings,
}),
tmpl: fc.Template,
log: log.New("alerting.notifier.telegram"),
images: fc.ImageStore,
ns: fc.NotificationService,
settings: settings,
}, nil
}
// Notify send an alert notification to Telegram.
func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
// Create the cmd for sendMessage
cmd, err := tn.newWebhookSyncCmd("sendMessage", func(w *multipart.Writer) error {
msg, err := tn.buildTelegramMessage(ctx, as)
if err != nil {
return fmt.Errorf("failed to build message: %w", err)
}
for k, v := range msg {
fw, err := w.CreateFormField(k)
if err != nil {
return fmt.Errorf("failed to create form field: %w", err)
}
if _, err := fw.Write([]byte(v)); err != nil {
return fmt.Errorf("failed to write value: %w", err)
}
}
return nil
})
if err != nil {
return false, fmt.Errorf("failed to create telegram message: %w", err)
}
if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil {
return false, fmt.Errorf("failed to send telegram message: %w", err)
}
// Create the cmd to upload each image
_ = withStoredImages(ctx, tn.log, tn.images, func(index int, image ngmodels.Image) error {
cmd, err = tn.newWebhookSyncCmd("sendPhoto", func(w *multipart.Writer) error {
f, err := os.Open(image.Path)
if err != nil {
return fmt.Errorf("failed to open image: %w", err)
}
defer func() {
if err := f.Close(); err != nil {
tn.log.Warn("failed to close image", "error", err)
}
}()
fw, err := w.CreateFormFile("photo", image.Path)
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(fw, f); err != nil {
return fmt.Errorf("failed to write to form file: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("failed to create image: %w", err)
}
if err := tn.ns.SendWebhookSync(ctx, cmd); err != nil {
return fmt.Errorf("failed to upload image to telegram: %w", err)
}
return nil
}, as...)
return true, nil
}
func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*types.Alert) (map[string]string, error) {
var tmplErr error
defer func() {
if tmplErr != nil {
tn.log.Warn("failed to template Telegram message", "error", tmplErr)
}
}()
tmpl, _ := TmplText(ctx, tn.tmpl, as, tn.log, &tmplErr)
// Telegram supports 4096 chars max
messageText, truncated := TruncateInRunes(tmpl(tn.settings.Message), telegramMaxMessageLenRunes)
if truncated {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return nil, err
}
tn.log.Warn("Truncated message", "alert", key, "max_runes", telegramMaxMessageLenRunes)
}
m := make(map[string]string)
m["text"] = messageText
m["parse_mode"] = "html"
return m, nil
}
func (tn *TelegramNotifier) newWebhookSyncCmd(action string, fn func(writer *multipart.Writer) error) (*models.SendWebhookSync, error) {
b := bytes.Buffer{}
w := multipart.NewWriter(&b)
boundary := GetBoundary()
if boundary != "" {
if err := w.SetBoundary(boundary); err != nil {
return nil, err
}
}
fw, err := w.CreateFormField("chat_id")
if err != nil {
return nil, err
}
if _, err := fw.Write([]byte(tn.settings.ChatID)); err != nil {
return nil, err
}
if err := fn(w); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to close multipart: %w", err)
}
cmd := &models.SendWebhookSync{
Url: fmt.Sprintf(TelegramAPIURL, tn.settings.BotToken, action),
Body: b.String(),
HttpMethod: "POST",
HttpHeader: map[string]string{
"Content-Type": w.FormDataContentType(),
},
}
return cmd, nil
}
func (tn *TelegramNotifier) SendResolved() bool {
return !tn.GetDisableResolveMessage()
}