mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 13:09:22 -06:00
157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
"github.com/prometheus/prometheus/promql"
|
|
"github.com/prometheus/prometheus/promql/parser"
|
|
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
type instantQueryResponse struct {
|
|
Status string `json:"status"`
|
|
Data queryData `json:"data,omitempty"`
|
|
ErrorType string `json:"errorType,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type queryData struct {
|
|
ResultType parser.ValueType `json:"resultType"`
|
|
Result json.RawMessage `json:"result"`
|
|
vector vector `json:"-"`
|
|
scalar scalar `json:"-"`
|
|
}
|
|
|
|
type scalar promql.Scalar
|
|
|
|
func (s *scalar) UnmarshalJSON(b []byte) error {
|
|
var xs []any
|
|
if err := json.Unmarshal(b, &xs); err != nil {
|
|
return err
|
|
}
|
|
// scalars are encoded like `[ts/1000, "value"]`
|
|
if len(xs) != 2 {
|
|
return fmt.Errorf("unexpected number of scalar encoded values: %d", len(xs))
|
|
}
|
|
ts, ok := xs[0].(float64)
|
|
if !ok {
|
|
return fmt.Errorf("first value in scalar uncoercible to timestamp: %v", xs[0])
|
|
}
|
|
s.T = int64(ts) * 1000
|
|
v, ok := xs[1].(string)
|
|
if !ok {
|
|
return fmt.Errorf("second value in scalar not string encoded: %v", xs[1])
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.V = f
|
|
return nil
|
|
}
|
|
|
|
func (d *queryData) UnmarshalJSON(b []byte) error {
|
|
type plain queryData
|
|
if err := json.Unmarshal(b, (*plain)(d)); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch d.ResultType {
|
|
case parser.ValueTypeScalar:
|
|
return json.Unmarshal(d.Result, &d.scalar)
|
|
case parser.ValueTypeVector:
|
|
return json.Unmarshal(d.Result, &d.vector)
|
|
default:
|
|
return fmt.Errorf("unexpected response type: %s", d.ResultType)
|
|
}
|
|
}
|
|
|
|
type sample struct {
|
|
Metric labels.Labels `json:"metric"`
|
|
Value scalar `json:"value"`
|
|
}
|
|
type vector []sample
|
|
|
|
func instantQueryResults(resp instantQueryResponse) (eval.Results, error) {
|
|
if resp.Error != "" || resp.Status != "success" {
|
|
return nil, errors.New(resp.Error)
|
|
}
|
|
|
|
switch resp.Data.ResultType {
|
|
case parser.ValueTypeScalar:
|
|
return eval.Results{{
|
|
Instance: map[string]string{},
|
|
State: eval.Alerting,
|
|
EvaluatedAt: TimeFromMillis(resp.Data.scalar.T),
|
|
EvaluationString: extractEvalStringFromProm(sample{
|
|
Value: resp.Data.scalar,
|
|
}),
|
|
}}, nil
|
|
case parser.ValueTypeVector:
|
|
results := make(eval.Results, 0, len(resp.Data.vector))
|
|
for _, s := range resp.Data.vector {
|
|
results = append(results, eval.Result{
|
|
Instance: s.Metric.Map(),
|
|
State: eval.Alerting,
|
|
EvaluatedAt: TimeFromMillis(s.Value.T),
|
|
EvaluationString: extractEvalStringFromProm(s),
|
|
})
|
|
}
|
|
return results, nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected response type: %s", resp.Data.ResultType)
|
|
}
|
|
}
|
|
|
|
func instantQueryResultsExtractor(r *response.NormalResponse) (any, error) {
|
|
contentType := r.Header().Get("Content-Type")
|
|
if !strings.Contains(contentType, "json") {
|
|
return nil, fmt.Errorf("unexpected content type from upstream. expected JSON, got %v", contentType)
|
|
}
|
|
var resp instantQueryResponse
|
|
err := json.Unmarshal(r.Body(), &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := instantQueryResults(resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
frame := res.AsDataFrame()
|
|
|
|
return util.DynMap{
|
|
"instances": []*data.Frame{&frame},
|
|
}, nil
|
|
}
|
|
|
|
// extractEvalStringFromProm is intended to mimic the functionality used in ngalert/eval
|
|
func extractEvalStringFromProm(s sample) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("[ ")
|
|
var ls string
|
|
if len(s.Metric) > 0 {
|
|
ls = s.Metric.String()
|
|
}
|
|
sb.WriteString(fmt.Sprintf("labels={%s} ", ls))
|
|
sb.WriteString(fmt.Sprintf("value=%v ", fmt.Sprintf("%v", s.Value.V)))
|
|
sb.WriteString("]")
|
|
return sb.String()
|
|
}
|
|
|
|
// TimeFromMillis Copied from https://github.com/grafana/mimir/blob/main/pkg/util/time.go as it doesn't seem worth it to import Mimir.
|
|
// TimeFromMillis is a helper to turn milliseconds -> time.Time
|
|
func TimeFromMillis(ms int64) time.Time {
|
|
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC()
|
|
}
|