mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-14 07:20:41 +01:00
#### Description
- Adds `GET|POST /prometheus/api/v1/query_range` and
`/prometheus/api/v1/query` (`pkg/prometheus/promapi`), following the
Prometheus HTTP API contract: float-unix or RFC3339 times, float-seconds
or duration-string durations, the `{status, data, errorType, error,
warnings, infos}` envelope with Prometheus' status codes, and the
11,000-point cap.
- The `/prometheus` prefix works as a drop-in Prometheus base URL:
Grafana's Prometheus data source, promtool, and the PromQL compliance
tester append `/api/v1/*` to a base URL, so they can point at SigNoz
unmodified. Same layout as Mimir/Cortex.
- Wired through `signoz.Handlers` (`prometheus.Handler` interface,
constructed in `NewHandlers`) like the other domain handlers.
- Range queries serve through the `RangeExecutor` capability when the
provider has it, so a clickhousev2-serving deployment transpiles through
these endpoints too.
- New `promapiconformance` integration suite: the frozen promqltest
corpus replayed against these endpoints with `prometheus::provider:
clickhousev2` — the two paths nothing else exercises (v2 as serving
provider, and this API surface). Instant cases go through `/query` with
a real `time` parameter. The `instant-coarse` corpus variants are
skipped — they exist only to encode instant evals as coarse ranges for
the v5 API, and their transpiled coarse-step serving is already covered
and ledgered by promqlconformance's clickhousev2 leg — so this suite
asserts zero divergences with no ledger of its own.
- Purely additive: the existing `GET /api/v1/query_range` and `GET
/api/v1/query` handlers are untouched. `openapi.yml` is generated and
these mux-registered routes are outside the generator, so their
documentation is the upstream Prometheus API contract they follow.
#### Additional Information
Final slice of the clickhouseprometheusv2 stack (#12323, #12324, #12325
— merged). Legacy endpoint removal, if ever, is a separate change after
usage drains.
193 lines
6.4 KiB
Go
193 lines
6.4 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TelemetrySelector(_ context.Context, resource coretypes.Resource, id string, _ valuer.UUID) ([]coretypes.Selector, error) {
|
|
values := telemetrytypes.NewTelemetryGrantSelectors(id)
|
|
|
|
selectors := make([]coretypes.Selector, 0, len(values))
|
|
for _, value := range values {
|
|
selector, err := resource.Type().Selector(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
selectors = append(selectors, selector)
|
|
}
|
|
|
|
return selectors, nil
|
|
}
|
|
|
|
func QueryRangeResources(ec coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
|
queries := gjson.GetBytes(ec.RequestBody, "compositeQuery.queries")
|
|
if !queries.IsArray() || len(queries.Array()) == 0 {
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "atleast one query is required")
|
|
}
|
|
|
|
variables, err := queryRangeVariables(ec.RequestBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refs := make([]coretypes.ResourceWithID, 0, len(queries.Array()))
|
|
seen := make(map[string]struct{})
|
|
for _, query := range queries.Array() {
|
|
queryRefs, err := resourcesForQuery(query, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ref := range queryRefs {
|
|
key := ref.Resource.Kind().String() + ":" + ref.ID
|
|
if _, ok := seen[key]; ok {
|
|
continue
|
|
}
|
|
seen[key] = struct{}{}
|
|
refs = append(refs, ref)
|
|
}
|
|
}
|
|
|
|
return refs, nil
|
|
}
|
|
|
|
func queryRangeVariables(body []byte) (map[string]qbtypes.VariableItem, error) {
|
|
variables := make(map[string]qbtypes.VariableItem)
|
|
|
|
raw := gjson.GetBytes(body, "variables")
|
|
if !raw.Exists() {
|
|
return variables, nil
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(raw.Raw), &variables); err != nil {
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid variables in query range request")
|
|
}
|
|
|
|
return variables, nil
|
|
}
|
|
|
|
// PromQLResources is the resource set of a bare PromQL query: metrics on
|
|
// the promql wildcard, the same ID resourcesForQuery assigns to a PromQL
|
|
// query inside a composite — one grant covers both entry points.
|
|
func PromQLResources(coretypes.ExtractorContext) ([]coretypes.ResourceWithID, error) {
|
|
return []coretypes.ResourceWithID{{
|
|
Resource: coretypes.ResourceTelemetryResourceMetrics,
|
|
ID: qbtypes.QueryTypePromQL.StringValue() + "/" + coretypes.WildCardSelectorString,
|
|
}}, nil
|
|
}
|
|
|
|
func resourcesForQuery(query gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
|
queryType := query.Get("type").String()
|
|
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
|
|
|
switch queryType {
|
|
case qbtypes.QueryTypeBuilder.StringValue(), qbtypes.QueryTypeSubQuery.StringValue():
|
|
return resourcesForBuilderQuery(queryType, query.Get("spec"), variables)
|
|
case qbtypes.QueryTypeBuilderAI.StringValue():
|
|
// always a traces query; the signal may be absent from the payload
|
|
return builderQueryResourceRefs(queryType, coretypes.ResourceTelemetryResourceTraces, query.Get("spec"), variables)
|
|
case qbtypes.QueryTypePromQL.StringValue():
|
|
return []coretypes.ResourceWithID{{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: typeWildcard}}, nil
|
|
case qbtypes.QueryTypeClickHouseSQL.StringValue():
|
|
return []coretypes.ResourceWithID{
|
|
{Resource: coretypes.ResourceTelemetryResourceLogs, ID: typeWildcard},
|
|
{Resource: coretypes.ResourceTelemetryResourceTraces, ID: typeWildcard},
|
|
{Resource: coretypes.ResourceTelemetryResourceMetrics, ID: typeWildcard},
|
|
{Resource: coretypes.ResourceTelemetryResourceMeterMetrics, ID: typeWildcard},
|
|
}, nil
|
|
case qbtypes.QueryTypeFormula.StringValue(), qbtypes.QueryTypeJoin.StringValue(), qbtypes.QueryTypeTraceOperator.StringValue():
|
|
return nil, nil
|
|
default:
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported query type %q", queryType)
|
|
}
|
|
}
|
|
|
|
func resourcesForBuilderQuery(queryType string, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
|
resource, err := builderQueryResource(spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return builderQueryResourceRefs(queryType, resource, spec, variables)
|
|
}
|
|
|
|
func builderQueryResourceRefs(queryType string, resource coretypes.Resource, spec gjson.Result, variables map[string]qbtypes.VariableItem) ([]coretypes.ResourceWithID, error) {
|
|
ids, err := builderQuerySelectors(queryType, spec.Get("filter.expression").String(), variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refs := make([]coretypes.ResourceWithID, 0, len(ids))
|
|
for _, id := range ids {
|
|
refs = append(refs, coretypes.ResourceWithID{Resource: resource, ID: id})
|
|
}
|
|
|
|
return refs, nil
|
|
}
|
|
|
|
func builderQueryResource(spec gjson.Result) (coretypes.Resource, error) {
|
|
source := spec.Get("source").String()
|
|
|
|
switch spec.Get("signal").String() {
|
|
case telemetrytypes.SignalTraces.StringValue():
|
|
return coretypes.ResourceTelemetryResourceTraces, nil
|
|
case telemetrytypes.SignalLogs.StringValue():
|
|
if source == telemetrytypes.SourceAudit.StringValue() {
|
|
return coretypes.ResourceTelemetryResourceAuditLogs, nil
|
|
}
|
|
return coretypes.ResourceTelemetryResourceLogs, nil
|
|
case telemetrytypes.SignalMetrics.StringValue():
|
|
if source == telemetrytypes.SourceMeter.StringValue() {
|
|
return coretypes.ResourceTelemetryResourceMeterMetrics, nil
|
|
}
|
|
return coretypes.ResourceTelemetryResourceMetrics, nil
|
|
default:
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported signal %q", spec.Get("signal").String())
|
|
}
|
|
}
|
|
|
|
func builderQuerySelectors(queryType, expression string, variables map[string]qbtypes.VariableItem) ([]string, error) {
|
|
typeWildcard := queryType + "/" + coretypes.WildCardSelectorString
|
|
|
|
if strings.TrimSpace(expression) == "" {
|
|
return []string{typeWildcard}, nil
|
|
}
|
|
|
|
normalized, err := NormalizeWhereClause(expression, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]string, 0)
|
|
for _, condition := range normalized.Conditions {
|
|
if !condition.TopLevel {
|
|
continue
|
|
}
|
|
|
|
key, ok := telemetrytypes.NewTelemetryGrantKey(condition.Key)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if condition.Operator == "=" || condition.Operator == "IN" {
|
|
for _, value := range condition.Values {
|
|
ids = append(ids, queryType+"/"+key+"/"+value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
return []string{typeWildcard}, nil
|
|
}
|
|
|
|
return ids, nil
|
|
}
|