2020-03-13 11:19:31 +01:00
|
|
|
package datasource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
2020-09-21 14:53:49 +02:00
|
|
|
"time"
|
2020-03-13 11:19:31 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type response struct {
|
|
|
|
Status string `json:"status"`
|
|
|
|
Data struct {
|
|
|
|
ResultType string `json:"resultType"`
|
|
|
|
Result []struct {
|
|
|
|
Labels map[string]string `json:"metric"`
|
|
|
|
TV [2]interface{} `json:"value"`
|
|
|
|
} `json:"result"`
|
|
|
|
} `json:"data"`
|
|
|
|
ErrorType string `json:"errorType"`
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r response) metrics() ([]Metric, error) {
|
|
|
|
var ms []Metric
|
|
|
|
var m Metric
|
|
|
|
var f float64
|
|
|
|
var err error
|
|
|
|
for i, res := range r.Data.Result {
|
|
|
|
f, err = strconv.ParseFloat(res.TV[1].(string), 64)
|
|
|
|
if err != nil {
|
2020-06-30 21:58:18 +02:00
|
|
|
return nil, fmt.Errorf("metric %v, unable to parse float64 from %s: %w", res, res.TV[1], err)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
m.Labels = nil
|
|
|
|
for k, v := range r.Data.Result[i].Labels {
|
2020-11-09 23:27:32 +01:00
|
|
|
m.AddLabel(k, v)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
m.Timestamp = int64(res.TV[0].(float64))
|
|
|
|
m.Value = f
|
|
|
|
ms = append(ms, m)
|
|
|
|
}
|
|
|
|
return ms, nil
|
|
|
|
}
|
|
|
|
|
2021-02-01 14:02:44 +01:00
|
|
|
type graphiteResponse []graphiteResponseTarget
|
|
|
|
|
|
|
|
type graphiteResponseTarget struct {
|
|
|
|
Target string `json:"target"`
|
|
|
|
Tags map[string]string `json:"tags"`
|
|
|
|
DataPoints [][2]float64 `json:"datapoints"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r graphiteResponse) metrics() []Metric {
|
|
|
|
var ms []Metric
|
|
|
|
for _, res := range r {
|
|
|
|
if len(res.DataPoints) < 1 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var m Metric
|
|
|
|
// add only last value to the result.
|
|
|
|
last := res.DataPoints[len(res.DataPoints)-1]
|
|
|
|
m.Value = last[0]
|
|
|
|
m.Timestamp = int64(last[1])
|
|
|
|
for k, v := range res.Tags {
|
|
|
|
m.AddLabel(k, v)
|
|
|
|
}
|
|
|
|
ms = append(ms, m)
|
|
|
|
}
|
|
|
|
return ms
|
|
|
|
}
|
|
|
|
|
2020-03-13 11:19:31 +01:00
|
|
|
// VMStorage represents vmstorage entity with ability to read and write metrics
|
|
|
|
type VMStorage struct {
|
2021-02-03 22:26:30 +01:00
|
|
|
c *http.Client
|
|
|
|
datasourceURL string
|
|
|
|
basicAuthUser string
|
|
|
|
basicAuthPass string
|
|
|
|
appendTypePrefix bool
|
|
|
|
lookBack time.Duration
|
|
|
|
queryStep time.Duration
|
2021-05-10 10:11:45 +02:00
|
|
|
roundDigits string
|
2021-04-30 08:46:03 +02:00
|
|
|
|
|
|
|
dataSourceType Type
|
|
|
|
evaluationInterval time.Duration
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
|
2021-02-01 14:02:44 +01:00
|
|
|
const queryPath = "/api/v1/query"
|
|
|
|
const graphitePath = "/render"
|
2020-09-21 14:53:49 +02:00
|
|
|
|
2021-02-03 22:26:30 +01:00
|
|
|
const prometheusPrefix = "/prometheus"
|
|
|
|
const graphitePrefix = "/graphite"
|
|
|
|
|
2021-04-28 22:41:15 +02:00
|
|
|
// QuerierParams params for Querier.
|
|
|
|
type QuerierParams struct {
|
2021-04-30 08:46:03 +02:00
|
|
|
DataSourceType *Type
|
|
|
|
EvaluationInterval time.Duration
|
2021-04-28 22:41:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clone makes clone of VMStorage, shares http client.
|
|
|
|
func (s *VMStorage) Clone() *VMStorage {
|
|
|
|
return &VMStorage{
|
|
|
|
c: s.c,
|
|
|
|
datasourceURL: s.datasourceURL,
|
|
|
|
basicAuthUser: s.basicAuthUser,
|
|
|
|
basicAuthPass: s.basicAuthPass,
|
|
|
|
lookBack: s.lookBack,
|
|
|
|
queryStep: s.queryStep,
|
|
|
|
appendTypePrefix: s.appendTypePrefix,
|
|
|
|
dataSourceType: s.dataSourceType,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ApplyParams - changes given querier params.
|
|
|
|
func (s *VMStorage) ApplyParams(params QuerierParams) *VMStorage {
|
|
|
|
if params.DataSourceType != nil {
|
|
|
|
s.dataSourceType = *params.DataSourceType
|
|
|
|
}
|
2021-04-30 09:31:45 +02:00
|
|
|
s.evaluationInterval = params.EvaluationInterval
|
2021-04-28 22:41:15 +02:00
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// BuildWithParams - implements interface.
|
|
|
|
func (s *VMStorage) BuildWithParams(params QuerierParams) Querier {
|
|
|
|
return s.Clone().ApplyParams(params)
|
|
|
|
}
|
|
|
|
|
2020-03-13 11:19:31 +01:00
|
|
|
// NewVMStorage is a constructor for VMStorage
|
2021-02-03 22:26:30 +01:00
|
|
|
func NewVMStorage(baseURL, basicAuthUser, basicAuthPass string, lookBack time.Duration, queryStep time.Duration, appendTypePrefix bool, c *http.Client) *VMStorage {
|
2020-03-13 11:19:31 +01:00
|
|
|
return &VMStorage{
|
2021-02-03 22:26:30 +01:00
|
|
|
c: c,
|
|
|
|
basicAuthUser: basicAuthUser,
|
|
|
|
basicAuthPass: basicAuthPass,
|
|
|
|
datasourceURL: strings.TrimSuffix(baseURL, "/"),
|
|
|
|
appendTypePrefix: appendTypePrefix,
|
|
|
|
lookBack: lookBack,
|
|
|
|
queryStep: queryStep,
|
2021-04-28 22:41:15 +02:00
|
|
|
dataSourceType: NewPrometheusType(),
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-30 08:46:03 +02:00
|
|
|
// Query executes the given query and returns parsed response
|
2021-04-28 22:41:15 +02:00
|
|
|
func (s *VMStorage) Query(ctx context.Context, query string) ([]Metric, error) {
|
2021-04-30 08:46:03 +02:00
|
|
|
req, err := s.prepareReq(query, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := s.do(ctx, req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2021-01-26 09:12:04 +01:00
|
|
|
}
|
2021-04-30 08:54:36 +02:00
|
|
|
defer func() {
|
|
|
|
_ = resp.Body.Close()
|
|
|
|
}()
|
2021-04-30 08:46:03 +02:00
|
|
|
|
|
|
|
parseFn := parsePrometheusResponse
|
|
|
|
if s.dataSourceType.name != prometheusType {
|
|
|
|
parseFn = parseGraphiteResponse
|
|
|
|
}
|
|
|
|
return parseFn(req, resp)
|
2021-02-01 14:02:44 +01:00
|
|
|
}
|
|
|
|
|
2021-04-30 08:46:03 +02:00
|
|
|
func (s *VMStorage) prepareReq(query string, timestamp time.Time) (*http.Request, error) {
|
2021-02-01 14:02:44 +01:00
|
|
|
req, err := http.NewRequest("POST", s.datasourceURL, nil)
|
2020-03-13 11:19:31 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-11-13 09:25:39 +01:00
|
|
|
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
2020-03-13 11:19:31 +01:00
|
|
|
if s.basicAuthPass != "" {
|
|
|
|
req.SetBasicAuth(s.basicAuthUser, s.basicAuthPass)
|
|
|
|
}
|
2021-04-30 08:46:03 +02:00
|
|
|
|
|
|
|
switch s.dataSourceType.name {
|
|
|
|
case "", prometheusType:
|
|
|
|
s.setPrometheusReqParams(req, query, timestamp)
|
|
|
|
case graphiteType:
|
|
|
|
s.setGraphiteReqParams(req, query, timestamp)
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("engine not found: %q", s.dataSourceType.name)
|
|
|
|
}
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *VMStorage) do(ctx context.Context, req *http.Request) (*http.Response, error) {
|
2020-03-13 11:19:31 +01:00
|
|
|
resp, err := s.c.Do(req.WithContext(ctx))
|
|
|
|
if err != nil {
|
2020-06-30 21:58:18 +02:00
|
|
|
return nil, fmt.Errorf("error getting response from %s: %w", req.URL, err)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
body, _ := ioutil.ReadAll(resp.Body)
|
2021-04-30 08:46:03 +02:00
|
|
|
_ = resp.Body.Close()
|
|
|
|
return nil, fmt.Errorf("unexpected response code %d for %s. Response body %s", resp.StatusCode, req.URL, body)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
2021-04-30 08:46:03 +02:00
|
|
|
return resp, nil
|
2021-02-01 14:02:44 +01:00
|
|
|
}
|
|
|
|
|
2021-04-30 08:46:03 +02:00
|
|
|
func (s *VMStorage) setPrometheusReqParams(r *http.Request, query string, timestamp time.Time) {
|
2021-02-03 22:26:30 +01:00
|
|
|
if s.appendTypePrefix {
|
|
|
|
r.URL.Path += prometheusPrefix
|
|
|
|
}
|
2021-02-01 14:02:44 +01:00
|
|
|
r.URL.Path += queryPath
|
|
|
|
q := r.URL.Query()
|
|
|
|
q.Set("query", query)
|
|
|
|
if s.lookBack > 0 {
|
2021-04-30 08:46:03 +02:00
|
|
|
timestamp = timestamp.Add(-s.lookBack)
|
|
|
|
}
|
|
|
|
if s.evaluationInterval > 0 {
|
|
|
|
// see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1232
|
|
|
|
timestamp = timestamp.Truncate(s.evaluationInterval)
|
2021-04-30 09:01:05 +02:00
|
|
|
// set step as evaluationInterval by default
|
|
|
|
q.Set("step", s.evaluationInterval.String())
|
2021-02-01 14:02:44 +01:00
|
|
|
}
|
2021-04-30 08:46:03 +02:00
|
|
|
q.Set("time", fmt.Sprintf("%d", timestamp.Unix()))
|
2021-04-30 09:01:05 +02:00
|
|
|
|
2021-02-01 14:02:44 +01:00
|
|
|
if s.queryStep > 0 {
|
2021-04-30 09:01:05 +02:00
|
|
|
// override step with user-specified value
|
2021-02-01 14:02:44 +01:00
|
|
|
q.Set("step", s.queryStep.String())
|
|
|
|
}
|
2021-05-10 10:11:45 +02:00
|
|
|
if s.roundDigits != "" {
|
|
|
|
q.Set("round_digits", s.roundDigits)
|
|
|
|
}
|
2021-02-01 14:02:44 +01:00
|
|
|
r.URL.RawQuery = q.Encode()
|
|
|
|
}
|
|
|
|
|
2021-04-30 08:46:03 +02:00
|
|
|
func (s *VMStorage) setGraphiteReqParams(r *http.Request, query string, timestamp time.Time) {
|
2021-02-03 22:26:30 +01:00
|
|
|
if s.appendTypePrefix {
|
|
|
|
r.URL.Path += graphitePrefix
|
|
|
|
}
|
2021-02-01 14:02:44 +01:00
|
|
|
r.URL.Path += graphitePath
|
|
|
|
q := r.URL.Query()
|
|
|
|
q.Set("format", "json")
|
|
|
|
q.Set("target", query)
|
|
|
|
from := "-5min"
|
|
|
|
if s.lookBack > 0 {
|
2021-04-30 08:46:03 +02:00
|
|
|
lookBack := timestamp.Add(-s.lookBack)
|
2021-02-01 14:02:44 +01:00
|
|
|
from = strconv.FormatInt(lookBack.Unix(), 10)
|
|
|
|
}
|
|
|
|
q.Set("from", from)
|
|
|
|
q.Set("until", "now")
|
|
|
|
r.URL.RawQuery = q.Encode()
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
statusSuccess, statusError, rtVector = "success", "error", "vector"
|
|
|
|
)
|
|
|
|
|
|
|
|
func parsePrometheusResponse(req *http.Request, resp *http.Response) ([]Metric, error) {
|
2020-03-13 11:19:31 +01:00
|
|
|
r := &response{}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
|
2021-02-01 14:02:44 +01:00
|
|
|
return nil, fmt.Errorf("error parsing prometheus metrics for %s: %w", req.URL, err)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
if r.Status == statusError {
|
|
|
|
return nil, fmt.Errorf("response error, query: %s, errorType: %s, error: %s", req.URL, r.ErrorType, r.Error)
|
|
|
|
}
|
|
|
|
if r.Status != statusSuccess {
|
2020-06-30 21:58:18 +02:00
|
|
|
return nil, fmt.Errorf("unknown status: %s, Expected success or error ", r.Status)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
if r.Data.ResultType != rtVector {
|
2020-10-07 16:59:50 +02:00
|
|
|
return nil, fmt.Errorf("unknown result type:%s. Expected vector", r.Data.ResultType)
|
2020-03-13 11:19:31 +01:00
|
|
|
}
|
|
|
|
return r.metrics()
|
|
|
|
}
|
2021-02-01 14:02:44 +01:00
|
|
|
|
|
|
|
func parseGraphiteResponse(req *http.Request, resp *http.Response) ([]Metric, error) {
|
|
|
|
r := &graphiteResponse{}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(r); err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing graphite metrics for %s: %w", req.URL, err)
|
|
|
|
}
|
|
|
|
return r.metrics(), nil
|
|
|
|
}
|