mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 21:19:28 -06:00
6768c6c059
Removes the public variable setting.SecretKey plus some other ones. Introduces some new functions for creating setting.Cfg.
169 lines
5.1 KiB
Go
169 lines
5.1 KiB
Go
package screenshot
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/rendering"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
const (
|
|
namespace = "grafana"
|
|
subsystem = "screenshot"
|
|
)
|
|
|
|
var (
|
|
ErrScreenshotsUnavailable = errors.New("screenshots unavailable")
|
|
)
|
|
|
|
// Screenshot represents a path to a screenshot on disk.
|
|
type Screenshot struct {
|
|
Path string
|
|
}
|
|
|
|
type screenshotFunc func(ctx context.Context, opts ScreenshotOptions) (*Screenshot, error)
|
|
|
|
// ScreenshotService is an interface for taking screenshots.
|
|
//
|
|
//go:generate mockgen -destination=screenshot_mock.go -package=screenshot github.com/grafana/grafana/pkg/services/screenshot ScreenshotService
|
|
type ScreenshotService interface {
|
|
Take(ctx context.Context, opts ScreenshotOptions) (*Screenshot, error)
|
|
}
|
|
|
|
// HeadlessScreenshotService takes screenshots using a headless browser.
|
|
type HeadlessScreenshotService struct {
|
|
cfg *setting.Cfg
|
|
ds dashboards.DashboardService
|
|
rs rendering.Service
|
|
|
|
duration prometheus.Histogram
|
|
failures *prometheus.CounterVec
|
|
successes prometheus.Counter
|
|
}
|
|
|
|
func NewHeadlessScreenshotService(cfg *setting.Cfg, ds dashboards.DashboardService, rs rendering.Service, r prometheus.Registerer) ScreenshotService {
|
|
return &HeadlessScreenshotService{
|
|
cfg: cfg,
|
|
ds: ds,
|
|
rs: rs,
|
|
duration: promauto.With(r).NewHistogram(prometheus.HistogramOpts{
|
|
Name: "duration_seconds",
|
|
Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10, 15},
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
}),
|
|
failures: promauto.With(r).NewCounterVec(prometheus.CounterOpts{
|
|
Name: "failures_total",
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
}, []string{"reason"}),
|
|
successes: promauto.With(r).NewCounter(prometheus.CounterOpts{
|
|
Name: "successes_total",
|
|
Namespace: namespace,
|
|
Subsystem: subsystem,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Take returns a screenshot of the panel. It returns an error if either the panel,
|
|
// or the dashboard that contains the panel, does not exist, or the screenshot could not be
|
|
// taken due to an error. It uses both the context and the timeout in ScreenshotOptions,
|
|
// however the same context is used for both database queries and the request to the
|
|
// rendering service, while the timeout in ScreenshotOptions is passed to the rendering service
|
|
// where it is used as a client timeout. It is not recommended to pass a context without a deadline
|
|
// and the context deadline should be at least as long as the timeout in ScreenshotOptions.
|
|
func (s *HeadlessScreenshotService) Take(ctx context.Context, opts ScreenshotOptions) (*Screenshot, error) {
|
|
start := time.Now()
|
|
defer func() { s.duration.Observe(time.Since(start).Seconds()) }()
|
|
|
|
q := dashboards.GetDashboardQuery{OrgID: opts.OrgID, UID: opts.DashboardUID}
|
|
dashboard, err := s.ds.GetDashboard(ctx, &q)
|
|
if err != nil {
|
|
s.instrumentError(err)
|
|
return nil, err
|
|
}
|
|
|
|
opts = opts.SetDefaults()
|
|
|
|
u := url.URL{}
|
|
u.Path = path.Join("d-solo", dashboard.UID, dashboard.Slug)
|
|
p := u.Query()
|
|
p.Add("orgId", strconv.FormatInt(dashboard.OrgID, 10))
|
|
p.Add("panelId", strconv.FormatInt(opts.PanelID, 10))
|
|
if opts.From != "" && opts.To != "" {
|
|
p.Add("from", opts.From)
|
|
p.Add("to", opts.To)
|
|
}
|
|
u.RawQuery = p.Encode()
|
|
|
|
renderOpts := rendering.Opts{
|
|
AuthOpts: rendering.AuthOpts{
|
|
OrgID: dashboard.OrgID,
|
|
OrgRole: org.RoleAdmin,
|
|
},
|
|
ErrorOpts: rendering.ErrorOpts{
|
|
ErrorConcurrentLimitReached: true,
|
|
ErrorRenderUnavailable: true,
|
|
},
|
|
TimeoutOpts: rendering.TimeoutOpts{
|
|
Timeout: opts.Timeout,
|
|
},
|
|
Width: opts.Width,
|
|
Height: opts.Height,
|
|
Theme: opts.Theme,
|
|
ConcurrentLimit: s.cfg.AlertingRenderLimit,
|
|
Path: u.String(),
|
|
}
|
|
|
|
result, err := s.rs.Render(ctx, renderOpts, nil)
|
|
if err != nil {
|
|
s.instrumentError(err)
|
|
return nil, fmt.Errorf("failed to take screenshot: %w", err)
|
|
}
|
|
|
|
defer s.successes.Inc()
|
|
screenshot := Screenshot{Path: result.FilePath}
|
|
return &screenshot, nil
|
|
}
|
|
|
|
func (s *HeadlessScreenshotService) instrumentError(err error) {
|
|
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
|
defer s.failures.With(prometheus.Labels{
|
|
"reason": "dashboard_not_found",
|
|
}).Inc()
|
|
} else if errors.Is(err, context.Canceled) {
|
|
defer s.failures.With(prometheus.Labels{
|
|
"reason": "context_canceled",
|
|
}).Inc()
|
|
} else {
|
|
defer s.failures.With(prometheus.Labels{
|
|
"reason": "error",
|
|
}).Inc()
|
|
}
|
|
}
|
|
|
|
// NoOpScreenshotService is a service that takes no-op screenshots.
|
|
type NoOpScreenshotService struct{}
|
|
|
|
func (s *NoOpScreenshotService) Take(_ context.Context, _ ScreenshotOptions) (*Screenshot, error) {
|
|
return &Screenshot{}, nil
|
|
}
|
|
|
|
// ScreenshotUnavailableService is a service that returns ErrScreenshotsUnavailable.
|
|
type ScreenshotUnavailableService struct{}
|
|
|
|
func (s *ScreenshotUnavailableService) Take(_ context.Context, _ ScreenshotOptions) (*Screenshot, error) {
|
|
return nil, ErrScreenshotsUnavailable
|
|
}
|