The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/api/promql_compat.go

156 lines
4.2 KiB

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()
}