Changes the time value of the response to be milliseconds, which I think is like the other data sources I have seen. Also if it sees the timerange values as numbers, assumes they are unixMS (and come from expressions, or our new backend.TimeRange type). When this timerange is used, the request is sent to graphite with from and until as epoch timestamps
241 lines
6.1 KiB
Go
241 lines
6.1 KiB
Go
package graphite
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/net/context/ctxhttp"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/tsdb"
|
|
"github.com/opentracing/opentracing-go"
|
|
)
|
|
|
|
type GraphiteExecutor struct {
|
|
HttpClient *http.Client
|
|
}
|
|
|
|
func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
|
return &GraphiteExecutor{}, nil
|
|
}
|
|
|
|
var glog = log.New("tsdb.graphite")
|
|
|
|
func init() {
|
|
tsdb.RegisterTsdbQueryEndpoint("graphite", NewGraphiteExecutor)
|
|
}
|
|
|
|
func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
|
result := &tsdb.Response{}
|
|
|
|
// This logic is used when called from Dashboard Alerting.
|
|
from := "-" + formatTimeRange(tsdbQuery.TimeRange.From)
|
|
until := formatTimeRange(tsdbQuery.TimeRange.To)
|
|
|
|
// This logic is used when called through server side expressions.
|
|
if isTimeRangeNumeric(tsdbQuery.TimeRange) {
|
|
var err error
|
|
from, until, err = epochMStoGraphiteTime(tsdbQuery.TimeRange)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var target string
|
|
|
|
formData := url.Values{
|
|
"from": []string{from},
|
|
"until": []string{until},
|
|
"format": []string{"json"},
|
|
"maxDataPoints": []string{"500"},
|
|
}
|
|
|
|
emptyQueries := make([]string, 0)
|
|
for _, query := range tsdbQuery.Queries {
|
|
glog.Debug("graphite", "query", query.Model)
|
|
currTarget := ""
|
|
if fullTarget, err := query.Model.Get("targetFull").String(); err == nil {
|
|
currTarget = fullTarget
|
|
} else {
|
|
currTarget = query.Model.Get("target").MustString()
|
|
}
|
|
if currTarget == "" {
|
|
glog.Debug("graphite", "empty query target", query.Model)
|
|
emptyQueries = append(emptyQueries, fmt.Sprintf("Query: %v has no target", query.Model))
|
|
continue
|
|
}
|
|
target = fixIntervalFormat(currTarget)
|
|
}
|
|
|
|
if target == "" {
|
|
glog.Error("No targets in query model", "models without targets", strings.Join(emptyQueries, "\n"))
|
|
return nil, errors.New("no query target found for the alert rule")
|
|
}
|
|
|
|
formData["target"] = []string{target}
|
|
|
|
if setting.Env == setting.Dev {
|
|
glog.Debug("Graphite request", "params", formData)
|
|
}
|
|
|
|
req, err := e.createRequest(dsInfo, formData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
httpClient, err := dsInfo.GetHttpClient()
|
|
if err != nil {
|
|
return nil, 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", dsInfo.OrgId)
|
|
|
|
defer span.Finish()
|
|
|
|
if err := opentracing.GlobalTracer().Inject(
|
|
span.Context(),
|
|
opentracing.HTTPHeaders,
|
|
opentracing.HTTPHeadersCarrier(req.Header)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := ctxhttp.Do(ctx, httpClient, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := e.parseResponse(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result.Results = make(map[string]*tsdb.QueryResult)
|
|
queryRes := tsdb.NewQueryResult()
|
|
|
|
for _, series := range data {
|
|
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
|
Name: series.Target,
|
|
Points: series.DataPoints,
|
|
})
|
|
|
|
if setting.Env == setting.Dev {
|
|
glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
|
|
}
|
|
}
|
|
|
|
result.Results["A"] = queryRes
|
|
return result, nil
|
|
}
|
|
|
|
func (e *GraphiteExecutor) 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 {
|
|
glog.Warn("Failed to close response body", "err", err)
|
|
}
|
|
}()
|
|
|
|
if res.StatusCode/100 != 2 {
|
|
glog.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 {
|
|
glog.Info("Failed to unmarshal graphite response", "error", err, "status", res.Status, "body", string(body))
|
|
return nil, err
|
|
}
|
|
|
|
for si := range data {
|
|
// Convert Response to timestamps MS
|
|
for pi, point := range data[si].DataPoints {
|
|
data[si].DataPoints[pi][1].Float64 = point[1].Float64 * 1000
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func (e *GraphiteExecutor) createRequest(dsInfo *models.DataSource, 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 {
|
|
glog.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")
|
|
if dsInfo.BasicAuth {
|
|
req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.DecryptedBasicAuthPassword())
|
|
}
|
|
|
|
return req, err
|
|
}
|
|
|
|
func formatTimeRange(input string) string {
|
|
if input == "now" {
|
|
return input
|
|
}
|
|
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(input, "now", ""), "m", "min"), "M", "mon")
|
|
}
|
|
|
|
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 isTimeRangeNumeric(tr *tsdb.TimeRange) bool {
|
|
if _, err := strconv.ParseInt(tr.From, 10, 64); err != nil {
|
|
return false
|
|
}
|
|
if _, err := strconv.ParseInt(tr.To, 10, 64); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func epochMStoGraphiteTime(tr *tsdb.TimeRange) (string, string, error) {
|
|
from, err := strconv.ParseInt(tr.From, 10, 64)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
to, err := strconv.ParseInt(tr.To, 10, 64)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return fmt.Sprintf("%d", from/1000), fmt.Sprintf("%d", to/1000), nil
|
|
}
|