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/util.go

256 lines
7.2 KiB

package channels
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"time"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/components/simplejson"
)
const (
FooterIconURL = "https://grafana.com/assets/img/fav32.png"
ColorAlertFiring = "#D63232"
ColorAlertResolved = "#36a64f"
// ImageStoreTimeout should be used by all callers for calles to `Images`
ImageStoreTimeout time.Duration = 500 * time.Millisecond
)
var (
// Provides current time. Can be overwritten in tests.
timeNow = time.Now
// ErrImagesDone is used to stop iteration of subsequent images. It should be
// returned from forEachFunc when either the intended image has been found or
// the maximum number of images has been iterated.
ErrImagesDone = errors.New("images done")
ErrImagesUnavailable = errors.New("alert screenshots are unavailable")
)
// For each alert, attempts to load the models.Image for an image token
// associated with the alert, then calls forEachFunc with the index of the
// alert and the retrieved image struct. If there is no image token, or the
// image does not exist, forEachFunc will be called with a nil value for the
// image. If forEachFunc returns an error, withStoredImages will return
// immediately. If there is a runtime error retrieving images from the image
// store, withStoredImages will attempt to continue executing, after logging
// a warning.
func withStoredImages(ctx context.Context, l log.Logger, imageStore ImageStore, forEachFunc func(index int, image *models.Image) error, alerts ...*types.Alert) error {
for i := range alerts {
err := withStoredImage(ctx, l, imageStore, forEachFunc, i, alerts...)
if err != nil {
// Stop iteration as forEachFunc has found the intended image or
// iterated the maximum number of images
if errors.Is(err, ErrImagesDone) {
return nil
}
return err
}
}
return nil
}
func withStoredImage(ctx context.Context, l log.Logger, imageStore ImageStore, imageFunc func(index int, image *models.Image) error, index int, alerts ...*types.Alert) error {
imgToken := getTokenFromAnnotations(alerts[index].Annotations)
if len(imgToken) == 0 {
err := imageFunc(index, nil)
if err != nil {
return err
}
}
timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout)
img, err := imageStore.GetImage(timeoutCtx, imgToken)
cancel()
if errors.Is(err, models.ErrImageNotFound) || errors.Is(err, ErrImagesUnavailable) {
err := imageFunc(index, nil)
if err != nil {
return err
}
} else if err != nil {
// Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist.
l.Warn("failed to retrieve image url from store", "err", err)
}
err = imageFunc(index, img)
if err != nil {
return err
}
return nil
}
// The path argument here comes from reading internal image storage, not user
// input, so we ignore the security check here.
//nolint:gosec
func openImage(path string) (io.ReadCloser, error) {
fp := filepath.Clean(path)
_, err := os.Stat(fp)
if os.IsNotExist(err) || os.IsPermission(err) {
return nil, models.ErrImageNotFound
}
f, err := os.Open(fp)
if err != nil {
return nil, err
}
return f, nil
}
func getTokenFromAnnotations(annotations model.LabelSet) string {
if value, ok := annotations[models.ScreenshotTokenAnnotation]; ok {
return string(value)
}
return ""
}
type UnavailableImageStore struct{}
// Get returns the image with the corresponding token, or ErrImageNotFound.
func (u *UnavailableImageStore) GetImage(ctx context.Context, token string) (*models.Image, error) {
return nil, ErrImagesUnavailable
}
type receiverInitError struct {
Reason string
Err error
Cfg NotificationChannelConfig
}
func (e receiverInitError) Error() string {
name := ""
if e.Cfg.Name != "" {
name = fmt.Sprintf("%q ", e.Cfg.Name)
}
s := fmt.Sprintf("failed to validate receiver %sof type %q: %s", name, e.Cfg.Type, e.Reason)
if e.Err != nil {
return fmt.Sprintf("%s: %s", s, e.Err.Error())
}
return s
}
func (e receiverInitError) Unwrap() error { return e.Err }
func getAlertStatusColor(status model.AlertStatus) string {
if status == model.AlertFiring {
return ColorAlertFiring
}
return ColorAlertResolved
}
type NotificationChannel interface {
notify.Notifier
notify.ResolvedSender
}
type NotificationChannelConfig struct {
OrgID int64 // only used internally
UID string `json:"uid"`
Name string `json:"name"`
Type string `json:"type"`
DisableResolveMessage bool `json:"disableResolveMessage"`
Settings *simplejson.Json `json:"settings"`
SecureSettings map[string][]byte `json:"secureSettings"`
}
type httpCfg struct {
body []byte
user string
password string
}
// sendHTTPRequest sends an HTTP request.
// Stubbable by tests.
var sendHTTPRequest = func(ctx context.Context, url *url.URL, cfg httpCfg, logger log.Logger) ([]byte, error) {
var reader io.Reader
if len(cfg.body) > 0 {
reader = bytes.NewReader(cfg.body)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), reader)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
if cfg.user != "" && cfg.password != "" {
request.Header.Set("Authorization", util.GetBasicAuthHeader(cfg.user, cfg.password))
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("User-Agent", "Grafana")
netTransport := &http.Transport{
TLSClientConfig: &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
},
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
}
netClient := &http.Client{
Timeout: time.Second * 30,
Transport: netTransport,
}
resp, err := netClient.Do(request)
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
logger.Warn("failed to close response body", "err", err)
}
}()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode/100 != 2 {
logger.Warn("HTTP request failed", "url", request.URL.String(), "statusCode", resp.Status, "body",
string(respBody))
return nil, fmt.Errorf("failed to send HTTP request - status code %d", resp.StatusCode)
}
logger.Debug("sending HTTP request succeeded", "url", request.URL.String(), "statusCode", resp.Status)
return respBody, nil
}
func joinUrlPath(base, additionalPath string, logger log.Logger) string {
u, err := url.Parse(base)
if err != nil {
logger.Debug("failed to parse URL while joining URL", "url", base, "err", err.Error())
return base
}
u.Path = path.Join(u.Path, additionalPath)
return u.String()
}
// GetBoundary is used for overriding the behaviour for tests
// and set a boundary for multipart body. DO NOT set this outside tests.
var GetBoundary = func() string {
return ""
}