Files
grafana/pkg/services/ngalert/writer/datasourcewriter.go
T

292 lines
8.1 KiB
Go

package writer
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/benbjohnson/clock"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/data"
gocache "github.com/patrickmn/go-cache"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/pluginsintegration/adapters"
)
const (
// We do not need to aggresively refresh data source settings, because a
// typical recording rule only writes every 60 seconds.
cacheExpiration = 30 * time.Second
// Time between cleaning expired data sources.
cacheCleanupInterval = 10 * time.Minute
)
type backendType string
const (
grafanaCloudPromType backendType = "grafanacloud-prom"
prometheusType backendType = "prometheus"
)
type DatasourceWriterConfig struct {
// Timeout is the maximum time to wait for a remote write to succeed.
Timeout time.Duration
// DetaultDatasourceUID is the data source used if no data source is specified.
// This exists to cater for upgrading from old versions of Grafana, where rule
// definitions may not have a target data source specified.
DefaultDatasourceUID string
// CustomHeaders is a map of optional custom HTTP headers
// to include in recording rule write requests.
CustomHeaders map[string]string
}
type PluginContextProvider interface {
GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error)
}
type DatasourceWriter struct {
cfg DatasourceWriterConfig
datasources datasources.DataSourceService
httpClientProvider HttpClientProvider
pluginContextProvider PluginContextProvider
clock clock.Clock
l log.Logger
metrics *metrics.RemoteWriter
writers *gocache.Cache
}
func NewDatasourceWriter(
cfg DatasourceWriterConfig,
datasources datasources.DataSourceService,
httpClientProvider HttpClientProvider,
pluginContextProvider PluginContextProvider,
clock clock.Clock,
l log.Logger,
metrics *metrics.RemoteWriter,
) *DatasourceWriter {
return &DatasourceWriter{
cfg: cfg,
datasources: datasources,
httpClientProvider: httpClientProvider,
pluginContextProvider: pluginContextProvider,
clock: clock,
l: l,
metrics: metrics,
writers: gocache.New(cacheExpiration, cacheCleanupInterval),
}
}
func (w *DatasourceWriter) decrypt(ds *datasources.DataSource) (map[string]string, error) {
decryptedJsonData, err := w.datasources.DecryptedValues(context.Background(), ds)
if err != nil {
w.l.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData, err
}
func getPrometheusType(ds *datasources.DataSource) string {
if ds.JsonData == nil {
return ""
}
jsonData := ds.JsonData.Get("prometheusType")
if jsonData == nil {
return ""
}
str, err := jsonData.String()
if err != nil {
return ""
}
return str
}
func getRemoteWriteURL(ds *datasources.DataSource) (*url.URL, error) {
u, err := url.Parse(ds.URL)
if err != nil {
return nil, err
}
if getPrometheusType(ds) == "Prometheus" {
return u.JoinPath("/api/v1/write"), nil
}
// All other cases assume Mimir/Cortex, as these systems are much more likely to be
// used as a remote write target, where as Prometheus does not recommend it.
// Mimir/Cortex are more complicated, as Grafana has to be configured with the
// base URL for where the Prometheus API is located, e.g. /api/prom or /prometheus.
//
// - For "legacy" routes, /push is located on the same level as /api/v1/query.
//
// For example:
// Grafana will be configured with <host>/api/prom
// The query API is at /api/prom/api/v1/query
// The push API is at /api/prom/push
//
// - For "new" routes, /push is located at the Mimir root, not Prometheus root.
//
// For example:
// Grafana will be configured with e.g. <host>/prometheus
// The query API is at /prometheus/api/v1/query
// But push API is at /push
//
// Unfortunately, the prefixes can also be configured,
cleanPath := path.Clean(u.Path)
// If the suffix is /api/prom, assume Mimir/Cortex with legacy routes.
if strings.HasSuffix(cleanPath, "/api/prom") {
u.Path = path.Join(u.Path, "/push")
return u, nil
}
// If the suffix is /prometheus, assume Mimir/Cortex with new routes.
if strings.HasSuffix(cleanPath, "/prometheus") {
u.Path = path.Join(path.Dir(u.Path), "/api/v1/push")
return u, nil
}
// The user has configured an unknown prefix, so fall back to taking
// the host as the Mimir root. This is less than ideal.
u.Path = "/api/v1/push"
return u, nil
}
func (w *DatasourceWriter) makeWriter(ctx context.Context, orgID int64, dsUID string) (*PrometheusWriter, error) {
ds, err := w.datasources.GetDataSource(ctx, &datasources.GetDataSourceQuery{
UID: dsUID,
OrgID: orgID,
})
if err != nil {
return nil, err
}
if ds.Type != datasources.DS_PROMETHEUS {
return nil, errors.New("can only write to data sources of type prometheus")
}
is, err := adapters.ModelToInstanceSettings(ds, w.decrypt)
if err != nil {
return nil, err
}
httpClientCtx := ctx
if w.pluginContextProvider != nil {
pluginCtx, err := w.pluginContextProvider.GetWithDataSource(ctx, ds.Type, nil, ds)
if err != nil {
return nil, fmt.Errorf("failed to get plugin context: %w", err)
}
httpClientCtx = backend.WithGrafanaConfig(ctx, pluginCtx.GrafanaConfig)
} else {
// This should not happen, but if the plugin context provider is not set, log a warning.
w.l.Warn("Plugin context provider is not set for the data source writer, PDC-enabled data sources may not work correctly", "datasource_uid", dsUID, "datasource_type", ds.Type)
}
ho, err := is.HTTPClientOptions(httpClientCtx)
if err != nil {
return nil, err
}
u, err := getRemoteWriteURL(ds)
if err != nil {
return nil, err
}
headers := make(http.Header)
for k, v := range w.cfg.CustomHeaders {
headers.Add(k, v)
}
var backend backendType
if dsUID == string(grafanaCloudPromType) {
backend = grafanaCloudPromType
} else {
backend = prometheusType
}
cfg := PrometheusWriterConfig{
URL: u.String(),
HTTPOptions: httpclient.Options{
Timeouts: ho.Timeouts,
TLS: ho.TLS,
BasicAuth: ho.BasicAuth,
Header: headers,
ProxyOptions: ho.ProxyOptions,
},
Timeout: w.cfg.Timeout,
BackendType: backend,
}
if err != nil {
return nil, err
}
w.l.Debug("Created Prometheus remote writer",
"datasource_uid", dsUID,
"type", ds.Type,
"prometheusType", getPrometheusType(ds),
"url", cfg.URL,
"tls", cfg.HTTPOptions.TLS != nil,
"basic_auth", cfg.HTTPOptions.BasicAuth != nil,
"timeout", cfg.Timeout)
return NewPrometheusWriter(
cfg,
w.httpClientProvider,
w.clock,
w.l,
w.metrics)
}
func uidKey(orgID int64, uid string) string {
return fmt.Sprintf("%d-%s", orgID, uid)
}
func (w *DatasourceWriter) WriteDatasource(ctx context.Context, dsUID string, name string, t time.Time, frames data.Frames, orgID int64, extraLabels map[string]string) error {
if dsUID == "" {
if w.cfg.DefaultDatasourceUID == "" {
return errors.New("data source uid not specified and no default set")
}
dsUID = w.cfg.DefaultDatasourceUID
w.l.Debug("Using default data source for remote write",
"org_id", orgID, "datasource_uid", dsUID)
}
key := uidKey(orgID, dsUID)
var writer *PrometheusWriter
val, ok := w.writers.Get(key)
if ok {
var ok bool
writer, ok = val.(*PrometheusWriter)
if !ok {
return errors.New("type in cache not a Writer")
}
} else {
var err error
writer, err = w.makeWriter(ctx, orgID, dsUID)
if err != nil {
w.l.Error("Failed to create writer for data source",
"org_id", orgID, "datasource_uid", dsUID)
return err
}
w.writers.Set(key, writer, 0)
}
return writer.Write(ctx, name, t, frames, orgID, extraLabels)
}