Alerting: Add a general screenshot service and alerting-specific image service. (#49293)

This commit adds a pkg/services/screenshot package for taking and uploading screenshots of Grafana dashboards. It supports taking screenshots of both dashboards and individual panels within a dashboard, using the rendering service.

The screenshot package has the following services, most of which can be composed:

BrowserScreenshotService (Takes screenshots with headless Chrome)
CachableScreenshotService (Caches screenshots taken with another service such as BrowserScreenshotService)
NoopScreenshotService (A no-op screenshot service for tests)
SingleFlightScreenshotService (Prevents duplicate screenshots when taking screenshots of the same dashboard or panel in parallel)
ScreenshotUnavailableService (A screenshot service that returns ErrScreenshotsUnavailable)
UploadingScreenshotService (A screenshot service that uploads taken screenshots)

The screenshot package does not support wire dependency injection yet. ngalert constructs its own version of the service. See https://github.com/grafana/grafana/issues/49296

This PR also adds an ImageScreenshotService to ngAlert. This is used to take screenshots with a screenshotservice and then store their location reference for use by alert instances and notifiers.
This commit is contained in:
Joe Blubaugh
2022-05-22 22:33:49 +08:00
committed by GitHub
parent f06d9164a6
commit 687e79538b
24 changed files with 1473 additions and 38 deletions

View File

@@ -0,0 +1,96 @@
package store
import (
"context"
"errors"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
var (
// ErrImageNotFound is returned when the image does not exist.
ErrImageNotFound = errors.New("image not found")
)
type Image struct {
ID int64 `xorm:"pk autoincr 'id'"`
Token string `xorm:"token"`
Path string `xorm:"path"`
URL string `xorm:"url"`
CreatedAt time.Time `xorm:"created_at"`
ExpiresAt time.Time `xorm:"expires_at"`
}
// A XORM interface that lets us clean up our SQL session definition.
func (i *Image) TableName() string {
return "alert_image"
}
type ImageStore interface {
// Get returns the image with the token or ErrImageNotFound.
GetImage(ctx context.Context, token string) (*Image, error)
// Saves the image or returns an error.
SaveImage(ctx context.Context, img *Image) error
}
func (st DBstore) GetImage(ctx context.Context, token string) (*Image, error) {
var img Image
if err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
exists, err := sess.Where("token = ?", token).Get(&img)
if err != nil {
return fmt.Errorf("failed to get image: %w", err)
}
if !exists {
return ErrImageNotFound
}
return nil
}); err != nil {
return nil, err
}
return &img, nil
}
func (st DBstore) SaveImage(ctx context.Context, img *Image) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// TODO: Is this a good idea? Do we actually want to automatically expire
// rows? See issue https://github.com/grafana/grafana/issues/49366
img.ExpiresAt = TimeNow().Add(1 * time.Minute).UTC()
if img.ID == 0 { // xorm will fill this field on Insert.
token, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("failed to create token: %w", err)
}
img.Token = token.String()
img.CreatedAt = TimeNow().UTC()
if _, err := sess.Insert(img); err != nil {
return fmt.Errorf("failed to insert screenshot: %w", err)
}
} else {
affected, err := sess.ID(img.ID).Update(img)
if err != nil {
return fmt.Errorf("failed to update screenshot: %v", err)
}
if affected == 0 {
return fmt.Errorf("update statement had no effect")
}
}
return nil
})
}
//nolint:unused
func (st DBstore) DeleteExpiredImages(ctx context.Context) error {
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
n, err := sess.Where("expires_at < ?", TimeNow()).Delete(&Image{})
if err != nil {
return fmt.Errorf("failed to delete expired images: %w", err)
}
st.Logger.Info("deleted expired images", "n", n)
return err
})
}