Files
zeekayandHanzo Dev 20ec5d62ea routing: query service and gateway read a path segment, not gorilla's map
19 reads across three files stop naming gorilla to address a resource by id and
go through coretypes.Param: 13 in the query-service handler (rule id, integration
id, pipeline version, six funnel ids), 1 in the parser (trace id), 5 in the
gateway (key id, limit id). Registration is untouched — this is the read side.

Where a site had bound `vars := mux.Vars(r)` only to index it once, the map is
gone with it; a handler wants the VALUE, and keeping a map alive to hold one
string was the router leaking into the body.

The query half stays exactly where it was. ParseSearchTracesParams now reads the
trace id from the PATH and the span id from the QUERY through two different
calls, which is what they always were.

http_handler.go keeps `mux.NewRouter().UseEncodedPath()` and the websocket
upgrader: those are the internal tree and its transport, converted as one piece
with the other 70 registration files, not here.

parser.go drops the gorilla import entirely; gateway/handler.go likewise.

Tests: neither package had one that exercised a path segment, so both go green
whether the read works or not — this repo has already shipped that defect. Each
now drives a REAL router registered at the REAL template. The gateway rows go
router -> handler -> Gateway and assert the id the gateway was HANDED, so an
empty segment fails on the recorded value rather than on a status code. Under a
Param that returns "", all four tests go red; before them, both packages stayed
green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 18:53:20 -07:00

1010 lines
30 KiB
Go

package app
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"net/http"
"sort"
"strconv"
"strings"
"text/template"
"time"
o11yerrors "github.com/hanzoai/o11y/pkg/errors"
"github.com/hanzoai/o11y/pkg/types/coretypes"
"github.com/hanzoai/o11y/pkg/types/thirdpartyapitypes"
"log/slog"
"github.com/hanzoai/govaluate"
"github.com/hanzoai/o11y/pkg/query-service/app/integrations/messagingQueues/kafka"
queues2 "github.com/hanzoai/o11y/pkg/query-service/app/integrations/messagingQueues/queues"
promModel "github.com/prometheus/common/model"
"go.uber.org/multierr"
errorsV2 "github.com/hanzoai/o11y/pkg/errors"
"github.com/hanzoai/o11y/pkg/query-service/app/metrics"
"github.com/hanzoai/o11y/pkg/query-service/app/queryBuilder"
"github.com/hanzoai/o11y/pkg/query-service/common"
baseconstants "github.com/hanzoai/o11y/pkg/query-service/constants"
"github.com/hanzoai/o11y/pkg/query-service/model"
v3 "github.com/hanzoai/o11y/pkg/query-service/model/v3"
"github.com/hanzoai/o11y/pkg/query-service/postprocess"
"github.com/hanzoai/o11y/pkg/query-service/utils"
querytemplate "github.com/hanzoai/o11y/pkg/query-service/utils/queryTemplate"
"github.com/hanzoai/o11y/pkg/types/retentiontypes"
dsVariables "github.com/hanzoai/o11y/pkg/variables/datastore"
)
var allowedFunctions = []string{"count", "ratePerSec", "sum", "avg", "min", "max", "p50", "p90", "p95", "p99"}
func parseGetTopOperationsRequest(r *http.Request) (*model.GetTopOperationsParams, error) {
var postData *model.GetTopOperationsParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartTime, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndTime, "end")
if err != nil {
return nil, err
}
if len(postData.ServiceName) == 0 {
return nil, errors.New("serviceName param missing in query")
}
return postData, nil
}
func parseRegisterEventRequest(r *http.Request) (*model.RegisterEventParams, error) {
var postData *model.RegisterEventParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
// Validate the event type
if !postData.EventType.IsValid() {
return nil, errors.New("eventType param missing/incorrect in query")
}
if postData.EventType == model.TrackEvent && postData.EventName == "" {
return nil, errors.New("eventName param missing in query")
}
return postData, nil
}
func parseMetricsTime(s string) (time.Time, error) {
if t, err := strconv.ParseFloat(s, 64); err == nil {
s, ns := math.Modf(t)
return time.Unix(int64(s), int64(ns*float64(time.Second))), nil
// return time.Unix(0, t), nil
}
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return t, nil
}
return time.Time{}, fmt.Errorf("cannot parse %q to a valid timestamp", s)
}
func parseMetricsDuration(s string) (time.Duration, error) {
if d, err := strconv.ParseFloat(s, 64); err == nil {
ts := d * float64(time.Second)
if ts > float64(math.MaxInt64) || ts < float64(math.MinInt64) {
return 0, fmt.Errorf("cannot parse %q to a valid duration. It overflows int64", s)
}
return time.Duration(ts), nil
}
if d, err := promModel.ParseDuration(s); err == nil {
return time.Duration(d), nil
}
return 0, fmt.Errorf("cannot parse %q to a valid duration", s)
}
func parseInstantQueryMetricsRequest(r *http.Request) (*model.InstantQueryMetricsParams, *model.ApiError) {
var ts time.Time
if t := r.FormValue("time"); t != "" {
var err error
ts, err = parseMetricsTime(t)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
} else {
ts = time.Now()
}
return &model.InstantQueryMetricsParams{
Time: ts,
Query: r.FormValue("query"),
Stats: r.FormValue("stats"),
}, nil
}
func parseQueryRangeRequest(r *http.Request) (*model.QueryRangeParams, *model.ApiError) {
start, err := parseMetricsTime(r.FormValue("start"))
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
end, err := parseMetricsTime(r.FormValue("end"))
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
if end.Before(start) {
err := errors.New("end timestamp must not be before start time")
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
step, err := parseMetricsDuration(r.FormValue("step"))
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
if step <= 0 {
err := errors.New("zero or negative query resolution step widths are not accepted. Try a positive integer")
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
// For safety, limit the number of returned points per timeseries.
// This is sufficient for 60s resolution for a week or 1h resolution for a year.
if end.Sub(start)/step > 11000 {
err := errors.New("exceeded maximum resolution of 11,000 points per timeseries. Try decreasing the query resolution (?step=XX)")
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
queryRangeParams := model.QueryRangeParams{
Start: start,
End: end,
Step: step,
Query: r.FormValue("query"),
Stats: r.FormValue("stats"),
}
return &queryRangeParams, nil
}
func parseGetUsageRequest(r *http.Request) (*model.GetUsageParams, error) {
startTime, err := parseTime("start", r)
if err != nil {
return nil, err
}
endTime, err := parseTime("end", r)
if err != nil {
return nil, err
}
stepStr := r.URL.Query().Get("step")
if len(stepStr) == 0 {
return nil, errors.New("step param missing in query")
}
stepInt, err := strconv.Atoi(stepStr)
if err != nil {
return nil, errors.New("step param is not in correct format")
}
serviceName := r.URL.Query().Get("service")
stepHour := stepInt / 3600
getUsageParams := model.GetUsageParams{
StartTime: startTime.Format(time.RFC3339Nano),
EndTime: endTime.Format(time.RFC3339Nano),
Start: startTime,
End: endTime,
ServiceName: serviceName,
Period: fmt.Sprintf("PT%dH", stepHour),
StepHour: stepHour,
}
return &getUsageParams, nil
}
func parseGetServicesRequest(r *http.Request) (*model.GetServicesParams, error) {
var postData *model.GetServicesParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartTime, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndTime, "end")
if err != nil {
return nil, err
}
postData.Period = int(postData.End.Unix() - postData.Start.Unix())
return postData, nil
}
func ParseSearchTracesParams(r *http.Request) (*model.SearchTracesParams, error) {
params := &model.SearchTracesParams{}
params.TraceID = coretypes.Param(r, "traceId")
params.SpanID = r.URL.Query().Get("spanId")
levelUpStr := r.URL.Query().Get("levelUp")
levelDownStr := r.URL.Query().Get("levelDown")
SpanRenderLimitStr := r.URL.Query().Get("spanRenderLimit")
if levelUpStr == "" || levelUpStr == "null" {
levelUpStr = "0"
}
if levelDownStr == "" || levelDownStr == "null" {
levelDownStr = "0"
}
if SpanRenderLimitStr == "" || SpanRenderLimitStr == "null" {
SpanRenderLimitStr = baseconstants.SpanRenderLimitStr
}
levelUpInt, err := strconv.Atoi(levelUpStr)
if err != nil {
return nil, err
}
levelDownInt, err := strconv.Atoi(levelDownStr)
if err != nil {
return nil, err
}
SpanRenderLimitInt, err := strconv.Atoi(SpanRenderLimitStr)
if err != nil {
return nil, err
}
MaxSpansInTraceInt, err := strconv.Atoi(baseconstants.MaxSpansInTraceStr)
if err != nil {
return nil, err
}
params.LevelUp = levelUpInt
params.LevelDown = levelDownInt
params.SpansRenderLimit = SpanRenderLimitInt
params.MaxSpansInTrace = MaxSpansInTraceInt
return params, nil
}
func DoesExistInSlice(item string, list []string) bool {
for _, element := range list {
if item == element {
return true
}
}
return false
}
func parseListErrorsRequest(r *http.Request) (*model.ListErrorsParams, error) {
var allowedOrderParams = []string{"exceptionType", "exceptionCount", "firstSeen", "lastSeen", "serviceName"}
var allowedOrderDirections = []string{"ascending", "descending"}
var postData *model.ListErrorsParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
if postData.Limit == 0 {
return nil, fmt.Errorf("limit param cannot be empty from the query")
}
if len(postData.Order) > 0 && !DoesExistInSlice(postData.Order, allowedOrderDirections) {
return nil, fmt.Errorf("given order: %s is not allowed in query", postData.Order)
}
if len(postData.Order) > 0 && !DoesExistInSlice(postData.OrderParam, allowedOrderParams) {
return nil, fmt.Errorf("given orderParam: %s is not allowed in query", postData.OrderParam)
}
return postData, nil
}
func parseCountErrorsRequest(r *http.Request) (*model.CountErrorsParams, error) {
var postData *model.CountErrorsParams
err := json.NewDecoder(r.Body).Decode(&postData)
if err != nil {
return nil, err
}
postData.Start, err = parseTimeStr(postData.StartStr, "start")
if err != nil {
return nil, err
}
postData.End, err = parseTimeMinusBufferStr(postData.EndStr, "end")
if err != nil {
return nil, err
}
return postData, nil
}
func parseGetErrorRequest(r *http.Request) (*model.GetErrorParams, error) {
timestamp, err := parseTime("timestamp", r)
if err != nil {
return nil, err
}
groupID := r.URL.Query().Get("groupID")
if len(groupID) == 0 {
return nil, fmt.Errorf("groupID param cannot be empty from the query")
}
errorID := r.URL.Query().Get("errorID")
params := &model.GetErrorParams{
Timestamp: timestamp,
GroupID: groupID,
ErrorID: errorID,
}
return params, nil
}
func parseTimeStr(timeStr string, param string) (*time.Time, error) {
if len(timeStr) == 0 {
return nil, fmt.Errorf("%s param missing in query", param)
}
timeUnix, err := strconv.ParseInt(timeStr, 10, 64)
if err != nil || len(timeStr) == 0 {
return nil, fmt.Errorf("%s param is not in correct timestamp format", param)
}
timeFmt := time.Unix(0, timeUnix)
return &timeFmt, nil
}
func parseTimeMinusBufferStr(timeStr string, param string) (*time.Time, error) {
if len(timeStr) == 0 {
return nil, fmt.Errorf("%s param missing in query", param)
}
timeUnix, err := strconv.ParseInt(timeStr, 10, 64)
if err != nil || len(timeStr) == 0 {
return nil, fmt.Errorf("%s param is not in correct timestamp format", param)
}
timeUnixNow := time.Now().UnixNano()
if timeUnix > timeUnixNow-30000000000 {
timeUnix = timeUnix - 30000000000
}
timeFmt := time.Unix(0, timeUnix)
return &timeFmt, nil
}
func parseTime(param string, r *http.Request) (*time.Time, error) {
timeStr := r.URL.Query().Get(param)
if len(timeStr) == 0 {
return nil, fmt.Errorf("%s param missing in query", param)
}
timeUnix, err := strconv.ParseInt(timeStr, 10, 64)
if err != nil || len(timeStr) == 0 {
return nil, fmt.Errorf("%s param is not in correct timestamp format", param)
}
timeFmt := time.Unix(0, timeUnix)
return &timeFmt, nil
}
func parseTTLParams(r *http.Request) (*retentiontypes.TTLParams, error) {
// make sure either of the query params are present
typeTTL := r.URL.Query().Get("type")
delDuration := r.URL.Query().Get("duration")
coldStorage := r.URL.Query().Get("coldStorage")
toColdDuration := r.URL.Query().Get("toColdDuration")
if len(typeTTL) == 0 || len(delDuration) == 0 {
return nil, fmt.Errorf("type and duration param cannot be empty from the query")
}
// Validate the type parameter
if typeTTL != retentiontypes.TraceTTL && typeTTL != retentiontypes.MetricsTTL && typeTTL != retentiontypes.LogsTTL {
return nil, fmt.Errorf("type param should be metrics|traces|logs, got %v", typeTTL)
}
// Validate the TTL duration.
durationParsed, err := time.ParseDuration(delDuration)
if err != nil || durationParsed.Seconds() <= 0 {
return nil, fmt.Errorf("not a valid TTL duration %v", delDuration)
}
var toColdParsed time.Duration
// If some cold storage is provided, validate the cold storage move TTL.
if len(coldStorage) > 0 {
toColdParsed, err = time.ParseDuration(toColdDuration)
if err != nil || toColdParsed.Seconds() <= 0 {
return nil, fmt.Errorf("not a valid toCold TTL duration %v", toColdDuration)
}
if toColdParsed.Seconds() != 0 && toColdParsed.Seconds() >= durationParsed.Seconds() {
return nil, fmt.Errorf("delete TTL should be greater than cold storage move TTL")
}
}
return &retentiontypes.TTLParams{
Type: typeTTL,
DelDuration: int64(durationParsed.Seconds()),
ColdStorageVolume: coldStorage,
ToColdStorageDuration: int64(toColdParsed.Seconds()),
}, nil
}
func parseGetTTL(r *http.Request) (*retentiontypes.GetTTLParams, error) {
typeTTL := r.URL.Query().Get("type")
if len(typeTTL) == 0 {
return nil, fmt.Errorf("type param cannot be empty from the query")
} else {
// Validate the type parameter
if typeTTL != retentiontypes.TraceTTL && typeTTL != retentiontypes.MetricsTTL && typeTTL != retentiontypes.LogsTTL {
return nil, fmt.Errorf("type param should be metrics|traces|logs, got %v", typeTTL)
}
}
return &retentiontypes.GetTTLParams{Type: typeTTL}, nil
}
func parseAggregateAttributeRequest(r *http.Request) (*v3.AggregateAttributeRequest, error) {
var req v3.AggregateAttributeRequest
aggregateOperator := v3.AggregateOperator(r.URL.Query().Get("aggregateOperator"))
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
aggregateAttribute := r.URL.Query().Get("searchText")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 50
}
if dataSource != v3.DataSourceMetrics && dataSource != v3.DataSourceMeter {
if err := aggregateOperator.Validate(); err != nil {
return nil, err
}
}
if err := dataSource.Validate(); err != nil {
return nil, err
}
req = v3.AggregateAttributeRequest{
Operator: aggregateOperator,
SearchText: aggregateAttribute,
Limit: limit,
DataSource: dataSource,
}
return &req, nil
}
func parseQBFilterSuggestionsRequest(r *http.Request) (
*v3.QBFilterSuggestionsRequest, *model.ApiError,
) {
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
if err := dataSource.Validate(); err != nil {
return nil, model.BadRequest(err)
}
parsePositiveIntQP := func(
queryParam string, defaultValue uint64, maxValue uint64,
) (uint64, *model.ApiError) {
value := defaultValue
qpValue := r.URL.Query().Get(queryParam)
if len(qpValue) > 0 {
value, err := strconv.Atoi(qpValue)
if err != nil || value < 1 || value > int(maxValue) {
return 0, model.BadRequest(fmt.Errorf(
"invalid %s: %s", queryParam, qpValue,
))
}
}
return value, nil
}
attributesLimit, err := parsePositiveIntQP(
"attributesLimit",
baseconstants.DefaultFilterSuggestionsAttributesLimit,
baseconstants.MaxFilterSuggestionsAttributesLimit,
)
if err != nil {
return nil, err
}
examplesLimit, err := parsePositiveIntQP(
"examplesLimit",
baseconstants.DefaultFilterSuggestionsExamplesLimit,
baseconstants.MaxFilterSuggestionsExamplesLimit,
)
if err != nil {
return nil, err
}
var existingFilter *v3.FilterSet
existingFilterB64 := r.URL.Query().Get("existingFilter")
if len(existingFilterB64) > 0 {
decodedFilterJson, err := base64.RawURLEncoding.DecodeString(existingFilterB64)
if err != nil {
return nil, model.BadRequest(fmt.Errorf("couldn't base64 decode existingFilter: %w", err))
}
existingFilter = &v3.FilterSet{}
err = json.Unmarshal(decodedFilterJson, existingFilter)
if err != nil {
return nil, model.BadRequest(fmt.Errorf("couldn't JSON decode existingFilter: %w", err))
}
}
searchText := r.URL.Query().Get("searchText")
return &v3.QBFilterSuggestionsRequest{
DataSource: dataSource,
SearchText: searchText,
ExistingFilter: existingFilter,
AttributesLimit: attributesLimit,
ExamplesLimit: examplesLimit,
}, nil
}
func parseFilterAttributeKeyRequest(r *http.Request) (*v3.FilterAttributeKeyRequest, error) {
var req v3.FilterAttributeKeyRequest
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
aggregateOperator := v3.AggregateOperator(r.URL.Query().Get("aggregateOperator"))
aggregateAttribute := r.URL.Query().Get("aggregateAttribute")
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
tagType := v3.TagType(r.URL.Query().Get("tagType"))
// empty string is a valid tagType
// i.e retrieve all attributes
if tagType != "" {
// what is happening here?
// if tagType is undefined(uh oh javascript) or any invalid value, set it to empty string
// instead of failing the request. Ideally, we should fail the request.
// but we are not doing that to maintain backward compatibility.
if err := tagType.Validate(); err != nil {
// if the tagType is invalid, set it to empty string
tagType = ""
}
}
if err != nil {
limit = 50
}
if err := dataSource.Validate(); err != nil {
return nil, err
}
if dataSource != v3.DataSourceMetrics && dataSource != v3.DataSourceMeter {
if err := aggregateOperator.Validate(); err != nil {
return nil, err
}
}
req = v3.FilterAttributeKeyRequest{
DataSource: dataSource,
AggregateOperator: aggregateOperator,
AggregateAttribute: aggregateAttribute,
Limit: limit,
SearchText: r.URL.Query().Get("searchText"),
TagType: tagType,
}
return &req, nil
}
func parseFilterAttributeValueRequestBody(r *http.Request) (*v3.FilterAttributeValueRequest, error) {
var req v3.FilterAttributeValueRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
if err := req.Validate(); err != nil {
return nil, err
}
// offset by two windows periods for start for better results
req.StartTimeMillis = req.StartTimeMillis - time.Hour.Milliseconds()*6*2
req.EndTimeMillis = req.EndTimeMillis + time.Hour.Milliseconds()*6
return &req, nil
}
func parseFilterAttributeValueRequest(r *http.Request) (*v3.FilterAttributeValueRequest, error) {
var req v3.FilterAttributeValueRequest
dataSource := v3.DataSource(r.URL.Query().Get("dataSource"))
aggregateOperator := v3.AggregateOperator(r.URL.Query().Get("aggregateOperator"))
filterAttributeKeyDataType := v3.AttributeKeyDataType(r.URL.Query().Get("filterAttributeKeyDataType")) // can be empty
aggregateAttribute := r.URL.Query().Get("aggregateAttribute")
tagType := v3.TagType(r.URL.Query().Get("tagType")) // can be empty
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
if err != nil {
limit = 50
}
if err := dataSource.Validate(); err != nil {
return nil, err
}
if dataSource != v3.DataSourceMetrics {
if err := aggregateOperator.Validate(); err != nil {
return nil, err
}
}
req = v3.FilterAttributeValueRequest{
DataSource: dataSource,
AggregateOperator: aggregateOperator,
AggregateAttribute: aggregateAttribute,
TagType: tagType,
Limit: limit,
SearchText: r.URL.Query().Get("searchText"),
FilterAttributeKey: r.URL.Query().Get("attributeKey"),
FilterAttributeKeyDataType: filterAttributeKeyDataType,
}
return &req, nil
}
func validateQueryRangeParamsV3(qp *v3.QueryRangeParamsV3) error {
err := qp.CompositeQuery.Validate()
if err != nil {
return err
}
var expressions []string
for _, q := range qp.CompositeQuery.BuilderQueries {
expressions = append(expressions, q.Expression)
}
errs := validateExpressions(expressions, queryBuilder.EvalFuncs, qp.CompositeQuery)
if len(errs) > 0 {
return multierr.Combine(errs...)
}
return nil
}
// validateExpressions validates the math expressions using the list of
// allowed functions.
func validateExpressions(expressions []string, funcs map[string]govaluate.ExpressionFunction, cq *v3.CompositeQuery) []error {
var errs []error
for _, exp := range expressions {
evalExp, err := govaluate.NewEvaluableExpressionWithFunctions(exp, funcs)
if err != nil {
errs = append(errs, fmt.Errorf("invalid expression %s: %v", exp, err))
continue
}
for _, v := range evalExp.Vars() {
var hasVariable bool
for _, q := range cq.BuilderQueries {
if q.Expression == v {
hasVariable = true
break
}
}
if !hasVariable {
errs = append(errs, fmt.Errorf("unknown variable %s", v))
}
}
}
return errs
}
// chTransformQuery transforms the datastore query with the given variables
// it is used to check what would be the query if variables are selected as __all__.
// for now, this is just a pass through, but in the future, we will use it to
// dashboard variables
// TODO(srikanthccv): version based query replacement
func chTransformQuery(query string, variables map[string]interface{}) {
varsForTransform := make([]dsVariables.VariableValue, 0, len(variables))
for name := range variables {
varsForTransform = append(varsForTransform, dsVariables.VariableValue{
Name: name,
Values: []string{"__all__"},
IsSelectAll: true,
FieldType: "scalar",
})
}
transformer := dsVariables.NewQueryTransformer(query, varsForTransform)
transformedQuery, err := transformer.Transform()
if err != nil {
slog.Warn("failed to transform datastore query", "query", query, o11yerrors.Attr(err))
}
slog.Info("transformed datastore query", "transformed_query", transformedQuery, "original_query", query)
}
func ParseQueryRangeParams(r *http.Request) (*v3.QueryRangeParamsV3, *model.ApiError) {
var queryRangeParams *v3.QueryRangeParamsV3
// parse the request body
if err := json.NewDecoder(r.Body).Decode(&queryRangeParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
// sanitize the request body
queryRangeParams.CompositeQuery.Sanitize()
// validate the request body
if err := validateQueryRangeParamsV3(queryRangeParams); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
// Clamp the top-level Step for PromQL
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
if minStep := common.MinAllowedStepInterval(queryRangeParams.Start, queryRangeParams.End); queryRangeParams.Step < minStep {
queryRangeParams.Step = minStep
}
}
// prepare the variables for the corresponding query type
formattedVars := make(map[string]interface{})
for name, value := range queryRangeParams.Variables {
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
formattedVars[name] = metrics.PromFormattedValue(value)
} else if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeDatastoreSQL {
formattedVars[name] = utils.DatastoreFormattedValue(value)
}
}
// replace the variables in metrics builder filter item with actual value
// example: {"key": "host", "value": "{{ .host }}", "operator": "equals"} with
// variables {"host": "test"} will be replaced with {"key": "host", "value": "test", "operator": "equals"}
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
for _, query := range queryRangeParams.CompositeQuery.BuilderQueries {
// Formula query
// Check if the queries used in the expression can be joined
if query.QueryName != query.Expression {
expression, err := govaluate.NewEvaluableExpressionWithFunctions(query.Expression, postprocess.EvalFuncs())
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
// get the group keys for the vars
groupKeys := make(map[string][]string)
for _, v := range expression.Vars() {
if varQuery, ok := queryRangeParams.CompositeQuery.BuilderQueries[v]; ok {
groupKeys[v] = []string{}
for _, key := range varQuery.GroupBy {
groupKeys[v] = append(groupKeys[v], key.Key)
}
} else {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("unknown variable %s", v)}
}
}
params := make(map[string]interface{})
for k, v := range groupKeys {
params[k] = v
}
can, _, err := expression.CanJoin(params)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
if !can {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot join the given group keys")}
}
}
// If the step interval is less than the minimum allowed step interval, set it to the minimum allowed step interval
if minStep := common.MinAllowedStepInterval(queryRangeParams.Start, queryRangeParams.End); query.StepInterval < minStep {
query.StepInterval = minStep
}
if query.DataSource == v3.DataSourceMetrics {
// if the time range is greater than 1 day, and less than 1 week set the step interval to be multiple of 5 minutes
// if the time range is greater than 1 week, set the step interval to be multiple of 30 mins
start, end := queryRangeParams.Start, queryRangeParams.End
if end-start >= 24*time.Hour.Milliseconds() && end-start < 7*24*time.Hour.Milliseconds() {
query.StepInterval = int64(math.Round(float64(query.StepInterval)/300)) * 300
} else if end-start >= 7*24*time.Hour.Milliseconds() {
query.StepInterval = int64(math.Round(float64(query.StepInterval)/1800)) * 1800
}
}
query.SetShiftByFromFunc()
if query.Filters == nil || len(query.Filters.Items) == 0 {
continue
}
for idx := range query.Filters.Items {
item := &query.Filters.Items[idx]
value := item.Value
if value != nil {
switch x := value.(type) {
case string:
variableName := strings.Trim(x, "{[.$]}")
if _, ok := queryRangeParams.Variables[variableName]; ok {
item.Value = queryRangeParams.Variables[variableName]
}
case []interface{}:
if len(x) > 0 {
switch x[0].(type) {
case string:
variableName := strings.Trim(x[0].(string), "{[.$]}")
if _, ok := queryRangeParams.Variables[variableName]; ok {
item.Value = queryRangeParams.Variables[variableName]
}
}
}
}
}
if v3.FilterOperator(strings.ToLower((string(item.Operator)))) != v3.FilterOperatorIn && v3.FilterOperator(strings.ToLower((string(item.Operator)))) != v3.FilterOperatorNotIn {
// the value type should not be multiple values
if _, ok := item.Value.([]interface{}); ok {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("multiple values %s are not allowed for operator `%s` for key `%s`", item.Value, item.Operator, item.Key.Key)}
}
}
}
}
}
queryRangeParams.Variables = formattedVars
// prometheus instant query needs same timestamp
if queryRangeParams.CompositeQuery.PanelType == v3.PanelTypeValue &&
queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
queryRangeParams.Start = queryRangeParams.End
}
// replace go template variables in datastore query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeDatastoreSQL {
for _, chQuery := range queryRangeParams.CompositeQuery.DatastoreQueries {
if chQuery.Disabled {
continue
}
chTransformQuery(chQuery.Query, queryRangeParams.Variables)
keys := make([]string, 0, len(queryRangeParams.Variables))
querytemplate.AssignReservedVarsV3(queryRangeParams)
for k := range queryRangeParams.Variables {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return len(keys[i]) > len(keys[j])
})
for _, k := range keys {
chQuery.Query = strings.Replace(chQuery.Query, fmt.Sprintf("{{%s}}", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
chQuery.Query = strings.Replace(chQuery.Query, fmt.Sprintf("[[%s]]", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
chQuery.Query = strings.Replace(chQuery.Query, fmt.Sprintf("$%s", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
}
tmpl := template.New("datastore-query")
tmpl, err := tmpl.Parse(chQuery.Query)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
var query bytes.Buffer
// replace go template variables
err = tmpl.Execute(&query, queryRangeParams.Variables)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
chQuery.Query = query.String()
}
}
// replace go template variables in prometheus query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypePromQL {
for _, promQuery := range queryRangeParams.CompositeQuery.PromQueries {
if promQuery.Disabled {
continue
}
querytemplate.AssignReservedVarsV3(queryRangeParams)
keys := make([]string, 0, len(queryRangeParams.Variables))
for k := range queryRangeParams.Variables {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return len(keys[i]) > len(keys[j])
})
for _, k := range keys {
promQuery.Query = strings.Replace(promQuery.Query, fmt.Sprintf("{{%s}}", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
promQuery.Query = strings.Replace(promQuery.Query, fmt.Sprintf("[[%s]]", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
promQuery.Query = strings.Replace(promQuery.Query, fmt.Sprintf("$%s", k), fmt.Sprint(queryRangeParams.Variables[k]), -1)
}
tmpl := template.New("prometheus-query")
tmpl, err := tmpl.Parse(promQuery.Query)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
var query bytes.Buffer
err = tmpl.Execute(&query, queryRangeParams.Variables)
if err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: err}
}
promQuery.Query = query.String()
}
}
return queryRangeParams, nil
}
// ParseKafkaQueueBody parse for messaging queue params
func ParseKafkaQueueBody(r *http.Request) (*kafka.MessagingQueue, *model.ApiError) {
messagingQueue := new(kafka.MessagingQueue)
if err := json.NewDecoder(r.Body).Decode(messagingQueue); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
return messagingQueue, nil
}
// ParseQueueBody parses for any queue
func ParseQueueBody(r *http.Request) (*queues2.QueueListRequest, *model.ApiError) {
queue := new(queues2.QueueListRequest)
if err := json.NewDecoder(r.Body).Decode(queue); err != nil {
return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("cannot parse the request body: %v", err)}
}
return queue, nil
}
// ParseRequestBody for third party APIs
func ParseRequestBody(r *http.Request) (*thirdpartyapitypes.ThirdPartyApiRequest, error) {
req := new(thirdpartyapitypes.ThirdPartyApiRequest)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
return nil, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "cannot parse the request body: %v", err)
}
if err := req.Validate(); err != nil {
return nil, err
}
return req, nil
}