mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
324 lines
8.6 KiB
Go
324 lines
8.6 KiB
Go
package graphite
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/context/ctxhttp"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/opentracing/opentracing-go"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/httpclient"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
|
)
|
|
|
|
type Service struct {
|
|
logger log.Logger
|
|
im instancemgmt.InstanceManager
|
|
}
|
|
|
|
const (
|
|
pluginID = "graphite"
|
|
TargetFullModelField = "targetFull"
|
|
TargetModelField = "target"
|
|
)
|
|
|
|
func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, pluginStore plugins.Store) (*Service, error) {
|
|
s := &Service{
|
|
logger: log.New("tsdb.graphite"),
|
|
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
|
|
}
|
|
|
|
factory := coreplugin.New(backend.ServeOpts{
|
|
QueryDataHandler: s,
|
|
})
|
|
|
|
resolver := plugins.CoreDataSourcePathResolver(cfg, pluginID)
|
|
if err := pluginStore.AddWithFactory(context.Background(), pluginID, factory, resolver); err != nil {
|
|
s.logger.Error("Failed to register plugin", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
type datasourceInfo struct {
|
|
HTTPClient *http.Client
|
|
URL string
|
|
Id int64
|
|
}
|
|
|
|
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
|
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
|
opts, err := settings.HTTPClientOptions()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := httpClientProvider.New(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
model := datasourceInfo{
|
|
HTTPClient: client,
|
|
URL: settings.URL,
|
|
Id: settings.ID,
|
|
}
|
|
|
|
return model, nil
|
|
}
|
|
}
|
|
|
|
func (s *Service) getDSInfo(pluginCtx backend.PluginContext) (*datasourceInfo, error) {
|
|
i, err := s.im.Get(pluginCtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
instance := i.(datasourceInfo)
|
|
return &instance, nil
|
|
}
|
|
|
|
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
if len(req.Queries) == 0 {
|
|
return nil, fmt.Errorf("query contains no queries")
|
|
}
|
|
|
|
// get datasource info from context
|
|
dsInfo, err := s.getDSInfo(req.PluginContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// take the first query in the request list, since all query should share the same timerange
|
|
q := req.Queries[0]
|
|
|
|
/*
|
|
graphite doc about from and until, with sdk we are getting absolute instead of relative time
|
|
https://graphite-api.readthedocs.io/en/latest/api.html#from-until
|
|
*/
|
|
from, until := epochMStoGraphiteTime(q.TimeRange)
|
|
formData := url.Values{
|
|
"from": []string{from},
|
|
"until": []string{until},
|
|
"format": []string{"json"},
|
|
"maxDataPoints": []string{"500"},
|
|
}
|
|
|
|
// Calculate and get the last target of Graphite Request
|
|
var target string
|
|
emptyQueries := make([]string, 0)
|
|
for _, query := range req.Queries {
|
|
model, err := simplejson.NewJson(query.JSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.logger.Debug("graphite", "query", model)
|
|
currTarget := ""
|
|
if fullTarget, err := model.Get(TargetFullModelField).String(); err == nil {
|
|
currTarget = fullTarget
|
|
} else {
|
|
currTarget = model.Get(TargetModelField).MustString()
|
|
}
|
|
if currTarget == "" {
|
|
s.logger.Debug("graphite", "empty query target", model)
|
|
emptyQueries = append(emptyQueries, fmt.Sprintf("Query: %v has no target", model))
|
|
continue
|
|
}
|
|
target = fixIntervalFormat(currTarget)
|
|
}
|
|
|
|
var result = backend.QueryDataResponse{}
|
|
|
|
if target == "" {
|
|
s.logger.Error("No targets in query model", "models without targets", strings.Join(emptyQueries, "\n"))
|
|
return &result, errors.New("no query target found for the alert rule")
|
|
}
|
|
|
|
formData["target"] = []string{target}
|
|
|
|
if setting.Env == setting.Dev {
|
|
s.logger.Debug("Graphite request", "params", formData)
|
|
}
|
|
|
|
graphiteReq, err := s.createRequest(dsInfo, formData)
|
|
if err != nil {
|
|
return &result, err
|
|
}
|
|
|
|
span, ctx := opentracing.StartSpanFromContext(ctx, "graphite query")
|
|
span.SetTag("target", target)
|
|
span.SetTag("from", from)
|
|
span.SetTag("until", until)
|
|
span.SetTag("datasource_id", dsInfo.Id)
|
|
span.SetTag("org_id", req.PluginContext.OrgID)
|
|
|
|
defer span.Finish()
|
|
|
|
if err := opentracing.GlobalTracer().Inject(
|
|
span.Context(),
|
|
opentracing.HTTPHeaders,
|
|
opentracing.HTTPHeadersCarrier(graphiteReq.Header)); err != nil {
|
|
return &result, err
|
|
}
|
|
|
|
res, err := ctxhttp.Do(ctx, dsInfo.HTTPClient, graphiteReq)
|
|
if err != nil {
|
|
return &result, err
|
|
}
|
|
|
|
frames, err := s.toDataFrames(res)
|
|
if err != nil {
|
|
return &result, err
|
|
}
|
|
|
|
result = backend.QueryDataResponse{
|
|
Responses: make(backend.Responses),
|
|
}
|
|
|
|
result.Responses["A"] = backend.DataResponse{
|
|
Frames: frames,
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *Service) parseResponse(res *http.Response) ([]TargetResponseDTO, error) {
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := res.Body.Close(); err != nil {
|
|
s.logger.Warn("Failed to close response body", "err", err)
|
|
}
|
|
}()
|
|
|
|
if res.StatusCode/100 != 2 {
|
|
s.logger.Info("Request failed", "status", res.Status, "body", string(body))
|
|
return nil, fmt.Errorf("request failed, status: %s", res.Status)
|
|
}
|
|
|
|
var data []TargetResponseDTO
|
|
err = json.Unmarshal(body, &data)
|
|
if err != nil {
|
|
s.logger.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
|
return nil, err
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (s *Service) toDataFrames(response *http.Response) (frames data.Frames, error error) {
|
|
responseData, err := s.parseResponse(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
frames = data.Frames{}
|
|
for _, series := range responseData {
|
|
timeVector := make([]time.Time, 0, len(series.DataPoints))
|
|
values := make([]*float64, 0, len(series.DataPoints))
|
|
name := series.Target
|
|
|
|
for _, dataPoint := range series.DataPoints {
|
|
var timestamp, value, err = parseDataTimePoint(dataPoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timeVector = append(timeVector, timestamp)
|
|
values = append(values, value)
|
|
}
|
|
|
|
tags := make(map[string]string)
|
|
for name, value := range series.Tags {
|
|
switch value := value.(type) {
|
|
case string:
|
|
tags[name] = value
|
|
case float64:
|
|
tags[name] = strconv.FormatFloat(value, 'f', -1, 64)
|
|
}
|
|
}
|
|
|
|
frames = append(frames, data.NewFrame(name,
|
|
data.NewField("time", nil, timeVector),
|
|
data.NewField("value", tags, values).SetConfig(&data.FieldConfig{DisplayNameFromDS: name})))
|
|
|
|
if setting.Env == setting.Dev {
|
|
s.logger.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
|
|
}
|
|
}
|
|
return frames, nil
|
|
}
|
|
|
|
func (s *Service) createRequest(dsInfo *datasourceInfo, data url.Values) (*http.Request, error) {
|
|
u, err := url.Parse(dsInfo.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Path = path.Join(u.Path, "render")
|
|
|
|
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
s.logger.Info("Failed to create request", "error", err)
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
return req, err
|
|
}
|
|
|
|
func fixIntervalFormat(target string) string {
|
|
rMinute := regexp.MustCompile(`'(\d+)m'`)
|
|
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
|
|
return strings.ReplaceAll(m, "m", "min")
|
|
})
|
|
rMonth := regexp.MustCompile(`'(\d+)M'`)
|
|
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
|
|
return strings.ReplaceAll(M, "M", "mon")
|
|
})
|
|
return target
|
|
}
|
|
|
|
func epochMStoGraphiteTime(tr backend.TimeRange) (string, string) {
|
|
return fmt.Sprintf("%d", tr.From.UTC().Unix()), fmt.Sprintf("%d", tr.To.UTC().Unix())
|
|
}
|
|
|
|
/**
|
|
* Graphite should always return timestamp as a number but values might be nil when data is missing
|
|
*/
|
|
func parseDataTimePoint(dataTimePoint legacydata.DataTimePoint) (time.Time, *float64, error) {
|
|
if !dataTimePoint[1].Valid {
|
|
return time.Time{}, nil, errors.New("failed to parse data point timestamp")
|
|
}
|
|
|
|
timestamp := time.Unix(int64(dataTimePoint[1].Float64), 0).UTC()
|
|
|
|
if dataTimePoint[0].Valid {
|
|
var value = new(float64)
|
|
*value = dataTimePoint[0].Float64
|
|
return timestamp, value, nil
|
|
} else {
|
|
return timestamp, nil, nil
|
|
}
|
|
}
|