chore: add query builder types (#7940)

This commit is contained in:
Srikanth Chekuri 2025-05-16 00:00:01 +05:30 committed by GitHub
parent 9fbf111976
commit 03600f4d6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 684 additions and 34 deletions

View File

@ -0,0 +1,67 @@
package metrictypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
)
// Temporality is the temporality of the metric specified in OTLP data model
// Read more here https://opentelemetry.io/docs/specs/otel/metrics/data-model/#temporality
type Temporality struct {
valuer.String
}
var (
Delta = Temporality{valuer.NewString("delta")}
Cumulative = Temporality{valuer.NewString("cumulative")}
Unspecified = Temporality{valuer.NewString("")}
)
// Type is the type of the metric in OTLP data model
// Read more here https://opentelemetry.io/docs/specs/otel/metrics/data-model/#metric-points
type Type struct {
valuer.String
}
var (
GaugeType = Type{valuer.NewString("gauge")}
SumType = Type{valuer.NewString("sum")}
HistogramType = Type{valuer.NewString("histogram")}
SummaryType = Type{valuer.NewString("summary")}
ExpHistogramType = Type{valuer.NewString("exponential_histogram")}
UnspecifiedType = Type{valuer.NewString("")}
)
type TimeAggregation struct {
valuer.String
}
var (
TimeAggregationUnspecified = TimeAggregation{valuer.NewString("")}
TimeAggregationLatest = TimeAggregation{valuer.NewString("latest")}
TimeAggregationSum = TimeAggregation{valuer.NewString("sum")}
TimeAggregationAvg = TimeAggregation{valuer.NewString("avg")}
TimeAggregationMin = TimeAggregation{valuer.NewString("min")}
TimeAggregationMax = TimeAggregation{valuer.NewString("max")}
TimeAggregationCount = TimeAggregation{valuer.NewString("count")}
TimeAggregationCountDistinct = TimeAggregation{valuer.NewString("count_distinct")}
TimeAggregationRate = TimeAggregation{valuer.NewString("rate")}
TimeAggregationIncrease = TimeAggregation{valuer.NewString("increase")}
)
type SpaceAggregation struct {
valuer.String
}
var (
SpaceAggregationUnspecified = SpaceAggregation{valuer.NewString("")}
SpaceAggregationSum = SpaceAggregation{valuer.NewString("sum")}
SpaceAggregationAvg = SpaceAggregation{valuer.NewString("avg")}
SpaceAggregationMin = SpaceAggregation{valuer.NewString("min")}
SpaceAggregationMax = SpaceAggregation{valuer.NewString("max")}
SpaceAggregationCount = SpaceAggregation{valuer.NewString("count")}
SpaceAggregationPercentile50 = SpaceAggregation{valuer.NewString("p50")}
SpaceAggregationPercentile75 = SpaceAggregation{valuer.NewString("p75")}
SpaceAggregationPercentile90 = SpaceAggregation{valuer.NewString("p90")}
SpaceAggregationPercentile95 = SpaceAggregation{valuer.NewString("p95")}
SpaceAggregationPercentile99 = SpaceAggregation{valuer.NewString("p99")}
)

View File

@ -0,0 +1,190 @@
package querybuildertypesv5
import (
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/metrictypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Step struct{ time.Duration }
func (s *Step) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return nil
}
if b[0] == '"' { // "15s", "1m", ISO8601
var str string
if err := json.Unmarshal(b, &str); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid step")
}
d, err := time.ParseDuration(str)
if err != nil {
return errors.WrapInvalidInputf(
err,
errors.CodeInvalidInput,
"invalid step, expected a duration string (example: 15s, 1m, 1h), valid time units are ns, u, ms, s, m, h",
)
}
s.Duration = d
return nil
}
var sec float64 // 30 → 30s ; 0.5 → 500ms
if err := json.Unmarshal(b, &sec); err != nil {
return errors.WrapInvalidInputf(
err,
errors.CodeInvalidInput,
"invalid step, expected duration in seconds (example: 60 - 1 minute, 240 - 4 minutes, 3600 - 1 hour)",
)
}
s.Duration = time.Duration(sec * float64(time.Second))
return nil
}
func (s Step) MarshalJSON() ([]byte, error) {
// Emit humanfriendly string → "30s"
return json.Marshal(s.Duration.String())
}
// FilterOperator is the operator for the filter.
type FilterOperator int
const (
FilterOperatorUnknown FilterOperator = iota
FilterOperatorEqual
FilterOperatorNotEqual
FilterOperatorGreaterThan
FilterOperatorGreaterThanOrEq
FilterOperatorLessThan
FilterOperatorLessThanOrEq
FilterOperatorLike
FilterOperatorNotLike
FilterOperatorILike
FilterOperatorNotILike
FilterOperatorBetween
FilterOperatorNotBetween
FilterOperatorIn
FilterOperatorNotIn
FilterOperatorExists
FilterOperatorNotExists
FilterOperatorRegexp
FilterOperatorNotRegexp
FilterOperatorContains
FilterOperatorNotContains
)
type OrderDirection struct {
valuer.String
}
var (
OrderDirectionAsc = OrderDirection{valuer.NewString("asc")}
OrderDirectionDesc = OrderDirection{valuer.NewString("desc")}
)
type ReduceTo struct {
valuer.String
}
var (
ReduceToUnknown = ReduceTo{valuer.NewString("")}
ReduceToSum = ReduceTo{valuer.NewString("sum")}
ReduceToCount = ReduceTo{valuer.NewString("count")}
ReduceToAvg = ReduceTo{valuer.NewString("avg")}
ReduceToMin = ReduceTo{valuer.NewString("min")}
ReduceToMax = ReduceTo{valuer.NewString("max")}
ReduceToLast = ReduceTo{valuer.NewString("last")}
ReduceToMedian = ReduceTo{valuer.NewString("median")}
)
type Aggregation struct {
// aggregation expression - example: count(), sum(item_price), countIf(day > 10)
Expression string `json:"expression"`
// if any, it will be used as the alias of the aggregation in the result
Alias string `json:"alias,omitempty"`
}
type MetricAggregation struct {
// metric to query
MetricName string `json:"metricName"`
// temporality to apply to the query
Temporality metrictypes.Temporality `json:"temporality"`
// time aggregation to apply to the query
TimeAggregation metrictypes.TimeAggregation `json:"timeAggregation"`
// space aggregation to apply to the query
SpaceAggregation metrictypes.SpaceAggregation `json:"spaceAggregation"`
}
type Filter struct {
// expression to filter by following the filter syntax
Expression string `json:"expression"`
}
type GroupByKey struct {
telemetrytypes.TelemetryFieldKey
}
type Having struct {
// expression to filter by following the filter syntax
Expression string `json:"expression"`
}
type OrderByKey struct {
telemetrytypes.TelemetryFieldKey
}
// key to order by
type OrderBy struct {
// key to order by
Key OrderByKey `json:"key"`
// direction to order by
Direction OrderDirection `json:"direction"`
}
// secondary aggregation to apply to the query
type SecondaryAggregation struct {
// stepInterval of the query
// if not set, it will use the step interval of the primary aggregation
StepInterval Step `json:"stepInterval,omitempty"`
// expression to aggregate. example: count(), sum(item_price), countIf(day > 10)
Expression string `json:"expression"`
// if any, it will be used as the alias of the aggregation in the result
Alias string `json:"alias,omitempty"`
// groupBy fields to group by
GroupBy []GroupByKey `json:"groupBy,omitempty"`
// order by keys and directions
Order []OrderBy `json:"order,omitempty"`
// limit the maximum number of rows to return
Limit int `json:"limit,omitempty"`
// limitBy fields to limit by
LimitBy LimitBy `json:"limitBy,omitempty"`
}
type Function struct {
// name of the function
Name string `json:"name"`
// args is the arguments to the function
Args []struct {
// name of the argument
Name string `json:"name,omitempty"`
// value of the argument
Value string `json:"value"`
} `json:"args,omitempty"`
}
type LimitBy struct {
// keys to limit by
Keys []string `json:"keys"`
// value to limit by
Value string `json:"value"`
}

View File

@ -0,0 +1,58 @@
package querybuildertypesv5
import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
type QueryBuilderQuery struct {
// name of the query, mainly used when query is used in formula
Name string `json:"name"`
// stepInterval of the query
StepInterval Step `json:"stepInterval,omitempty"`
// signal to query
Signal telemetrytypes.Signal `json:"signal,omitempty"`
// we want to support multiple aggregations
// currently supported: []Aggregation, []MetricAggregation
Aggregations []any `json:"aggregations,omitempty"`
// disabled if true, the query will not be executed
Disabled bool `json:"disabled,omitempty"`
// search query is simple string
Filter *Filter `json:"filter,omitempty"`
// group by keys to group by
GroupBy []GroupByKey `json:"groupBy,omitempty"`
// order by keys and directions
Order []OrderBy `json:"order,omitempty"`
// select columns to select
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
// limit the maximum number of rows to return
Limit int `json:"limit,omitempty"`
// limitBy fields to limit by
LimitBy LimitBy `json:"limitBy,omitempty"`
// offset the number of rows to skip
// TODO: remove this once we have cursor-based pagination everywhere?
Offset int `json:"offset,omitempty"`
// cursor to paginate the query
Cursor string `json:"cursor,omitempty"`
// having clause to apply to the query
Having *Having `json:"having,omitempty"`
// secondary aggregation to apply to the query
// on top of the primary aggregation
SecondaryAggregations []SecondaryAggregation `json:"secondaryAggregations,omitempty"`
// functions to apply to the query
Functions []Function `json:"functions,omitempty"`
}

View File

@ -0,0 +1,9 @@
package querybuildertypesv5
// BucketCache is the only thing orchestrator cares about.
type BucketCache interface {
// cached portion + list of gaps to fetch
GetMissRanges(q Query) (cached Result, missing []TimeRange)
// store fresh buckets for future hits
Put(q Query, fresh Result)
}

View File

@ -0,0 +1,10 @@
package querybuildertypesv5
type ClickHouseQuery struct {
// name of the query
Name string `json:"name"`
// query to execute
Query string `json:"query"`
// disabled if true, the query will not be executed
Disabled bool `json:"disabled"`
}

View File

@ -0,0 +1,11 @@
package querybuildertypesv5
type QueryBuilderFormula struct {
// name of the formula
Name string `json:"name"`
// expression to apply to the query
Expression string `json:"expression"`
// functions to apply to the formula result
Functions []Function `json:"functions,omitempty"`
}

View File

@ -0,0 +1,27 @@
package querybuildertypesv5
import "github.com/SigNoz/signoz/pkg/valuer"
type FunctionName struct {
valuer.String
}
var (
FunctionNameCutOffMin = FunctionName{valuer.NewString("cutOffMin")}
FunctionNameCutOffMax = FunctionName{valuer.NewString("cutOffMax")}
FunctionNameClampMin = FunctionName{valuer.NewString("clampMin")}
FunctionNameClampMax = FunctionName{valuer.NewString("clampMax")}
FunctionNameAbsolute = FunctionName{valuer.NewString("absolute")}
FunctionNameRunningDiff = FunctionName{valuer.NewString("runningDiff")}
FunctionNameLog2 = FunctionName{valuer.NewString("log2")}
FunctionNameLog10 = FunctionName{valuer.NewString("log10")}
FunctionNameCumSum = FunctionName{valuer.NewString("cumSum")}
FunctionNameEWMA3 = FunctionName{valuer.NewString("ewma3")}
FunctionNameEWMA5 = FunctionName{valuer.NewString("ewma5")}
FunctionNameEWMA7 = FunctionName{valuer.NewString("ewma7")}
FunctionNameMedian3 = FunctionName{valuer.NewString("median3")}
FunctionNameMedian5 = FunctionName{valuer.NewString("median5")}
FunctionNameMedian7 = FunctionName{valuer.NewString("median7")}
FunctionNameTimeShift = FunctionName{valuer.NewString("timeShift")}
FunctionNameAnomaly = FunctionName{valuer.NewString("anomaly")}
)

View File

@ -0,0 +1,49 @@
package querybuildertypesv5
import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// JoinType is the SQLstyle join operator.
type JoinType struct{ valuer.String }
var (
JoinTypeInner = JoinType{valuer.NewString("inner")}
JoinTypeLeft = JoinType{valuer.NewString("left")}
JoinTypeRight = JoinType{valuer.NewString("right")}
JoinTypeFull = JoinType{valuer.NewString("full")}
JoinTypeCross = JoinType{valuer.NewString("cross")}
)
type QueryRef struct {
Name string `json:"name"`
}
type QueryBuilderJoin struct {
Name string `json:"name"`
Disabled bool `json:"disabled,omitempty"`
// references into flat registry of queries
Left QueryRef `json:"left"`
Right QueryRef `json:"right"`
// join type + condition ON
Type JoinType `json:"type"`
On string `json:"on"`
// primary aggregations: if empty ⇒ raw columns
// currently supported: []Aggregation, []MetricAggregation
Aggregations []any `json:"aggregations,omitempty"`
// select columns to select
SelectFields []telemetrytypes.TelemetryFieldKey `json:"selectFields,omitempty"`
// post-join clauses (also used for aggregated joins)
Filter *Filter `json:"filter,omitempty"`
GroupBy []GroupByKey `json:"groupBy,omitempty"`
Having *Having `json:"having,omitempty"`
Order []OrderBy `json:"order,omitempty"`
Limit int `json:"limit,omitempty"`
SecondaryAggregations []SecondaryAggregation `json:"secondaryAggregations,omitempty"`
Functions []Function `json:"functions,omitempty"`
}

View File

@ -0,0 +1,10 @@
package querybuildertypesv5
type PromQuery struct {
// name of the query
Name string `json:"name"`
// query to execute
Query string `json:"query"`
// disabled if true, the query will not be executed
Disabled bool `json:"disabled"`
}

View File

@ -1,4 +1,4 @@
package types
package querybuildertypesv5
import (
"context"
@ -15,39 +15,6 @@ var (
ErrInValues = errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "(not) in operator requires a list of values")
)
// FilterOperator is the operator for the filter.
type FilterOperator int
const (
FilterOperatorUnknown FilterOperator = iota
FilterOperatorEqual
FilterOperatorNotEqual
FilterOperatorGreaterThan
FilterOperatorGreaterThanOrEq
FilterOperatorLessThan
FilterOperatorLessThanOrEq
FilterOperatorLike
FilterOperatorNotLike
FilterOperatorILike
FilterOperatorNotILike
FilterOperatorBetween
FilterOperatorNotBetween
FilterOperatorIn
FilterOperatorNotIn
FilterOperatorExists
FilterOperatorNotExists
FilterOperatorRegexp
FilterOperatorNotRegexp
FilterOperatorContains
FilterOperatorNotContains
)
// ConditionBuilder is the interface for building the condition part of the query.
type ConditionBuilder interface {
// GetColumn returns the column for the given key.

View File

@ -0,0 +1,7 @@
package querybuildertypesv5
import "context"
type Querier interface {
QueryRange(ctx context.Context, req QueryRangeRequest) (QueryRangeResponse, error)
}

View File

@ -0,0 +1,29 @@
package querybuildertypesv5
import (
"context"
)
type Query interface {
// Fingerprint must return a deterministic key that uniquely identifies
// (query-text, params, step, etc..) but *not* the time range.
Fingerprint() string
// Window returns [from, to) in epochms so cache can slice/merge.
Window() (startMS, endMS uint64)
// Execute runs the query; implementors must be sideeffectfree.
Execute(ctx context.Context) (Result, error)
}
type Result struct {
Type RequestType
Value any // concrete Go value (to be type asserted based on the RequestType)
Stats ExecStats
}
type ExecStats struct {
RowsScanned int64 `json:"rowsScanned"`
BytesScanned int64 `json:"bytesScanned"`
DurationMs int64 `json:"durationMs"`
}
type TimeRange struct{ From, To uint64 } // ms since epoch

View File

@ -0,0 +1,17 @@
package querybuildertypesv5
import "github.com/SigNoz/signoz/pkg/valuer"
type QueryType struct {
valuer.String
}
var (
QueryTypeUnknown = QueryType{valuer.NewString("unknown")}
QueryTypeBuilder = QueryType{valuer.NewString("builder_query")}
QueryTypeFormula = QueryType{valuer.NewString("builder_formula")}
QueryTypeSubQuery = QueryType{valuer.NewString("builder_sub_query")}
QueryTypeJoin = QueryType{valuer.NewString("builder_join")}
QueryTypeClickHouseSQL = QueryType{valuer.NewString("clickhouse_sql")}
QueryTypePromQL = QueryType{valuer.NewString("promql")}
)

View File

@ -0,0 +1,6 @@
package querybuildertypesv5
// Renderer is the interface for rendering the result of a query.
type Renderer interface {
Render(res Result) (any, error)
}

View File

@ -0,0 +1,94 @@
package querybuildertypesv5
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
)
type QueryEnvelope struct {
// Name is the unique identifier for the query.
Name string `json:"name"`
// Type is the type of the query.
Type QueryType `json:"type"` // "builder_query" | "builder_formula" | "builder_sub_query" | "builder_join" | "promql" | "clickhouse_sql"
// Spec is the deferred decoding of the query if any.
Spec any `json:"spec"`
}
// implement custom json unmarshaler for the QueryEnvelope
func (q *QueryEnvelope) UnmarshalJSON(data []byte) error {
var shadow struct {
Name string `json:"name"`
Type QueryType `json:"type"`
Spec json.RawMessage `json:"spec"`
}
if err := json.Unmarshal(data, &shadow); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid query envelope")
}
q.Name = shadow.Name
q.Type = shadow.Type
// 2. Decode the spec based on the Type.
switch shadow.Type {
case QueryTypeBuilder, QueryTypeSubQuery:
var spec QueryBuilderQuery
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid builder query spec")
}
q.Spec = spec
case QueryTypeFormula:
var spec QueryBuilderFormula
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid formula spec")
}
q.Spec = spec
case QueryTypeJoin:
var spec QueryBuilderJoin
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid join spec")
}
q.Spec = spec
case QueryTypePromQL:
var spec PromQuery
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid PromQL spec")
}
q.Spec = spec
case QueryTypeClickHouseSQL:
var spec ClickHouseQuery
if err := json.Unmarshal(shadow.Spec, &spec); err != nil {
return errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid ClickHouse SQL spec")
}
q.Spec = spec
default:
return errors.WrapInvalidInputf(nil, errors.CodeInvalidInput, "unknown query type %q", shadow.Type)
}
return nil
}
type CompositeQuery struct {
// Queries is the queries to use for the request.
Queries []QueryEnvelope `json:"queries"`
}
type QueryRangeRequest struct {
// SchemaVersion is the version of the schema to use for the request payload.
SchemaVersion string `json:"schemaVersion"`
// Start is the start time of the query in epoch milliseconds.
Start uint64 `json:"start"`
// End is the end time of the query in epoch milliseconds.
End uint64 `json:"end"`
// RequestType is the type of the request.
RequestType RequestType `json:"requestType"`
// CompositeQuery is the composite query to use for the request.
CompositeQuery CompositeQuery `json:"compositeQuery"`
// Variables is the variables to use for the request.
Variables map[string]any `json:"variables,omitempty"`
}

View File

@ -0,0 +1,19 @@
package querybuildertypesv5
import "github.com/SigNoz/signoz/pkg/valuer"
type RequestType struct {
valuer.String
}
var (
RequestTypeUnknown = RequestType{valuer.NewString("")}
// Scalar result(s), example: number panel, and table panel
RequestTypeScalar = RequestType{valuer.NewString("scalar")}
// []Point (struct{TS int64; Val float64}), example: line/area/bar chart
RequestTypeTimeSeries = RequestType{valuer.NewString("time_series")}
// [][]any, SQL result set, but paginated, example: list view
RequestTypeRaw = RequestType{valuer.NewString("raw")}
// []Bucket (struct{Lower,Upper,Count float64}), example: histogram
RequestTypeDistribution = RequestType{valuer.NewString("distribution")}
)

View File

@ -0,0 +1,80 @@
package querybuildertypesv5
import (
"time"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type QueryRangeResponse struct {
Type RequestType `json:"type"`
Data any `json:"data"`
Meta ExecStats `json:"meta"`
}
type TimeSeriesData struct {
QueryName string `json:"queryName"`
Aggregations []AggregationBucket `json:"aggregations"`
}
type AggregationBucket struct {
Index int `json:"index"` // or string Alias
Alias string `json:"alias"`
Series []TimeSeries `json:"series"` // no extra nesting
}
type TimeSeries struct {
Labels []Label `json:"labels,omitempty"`
Values []TimeSeriesValue `json:"values"`
}
type Label struct {
Key telemetrytypes.TelemetryFieldKey `json:"key"`
Value any `json:"value"`
}
type TimeSeriesValue struct {
Timestamp int64 `json:"timestamp"`
Value float64 `json:"value,omitempty"`
// for the heatmap type chart
Values []float64 `json:"values,omitempty"`
Bucket Bucket `json:"bucket,omitempty"`
}
type Bucket struct {
Step float64 `json:"step"`
}
type ColumnType struct {
valuer.String
}
var (
// for the group by part of the query
ColumnTypeGroup = ColumnType{valuer.NewString("group")}
// for the aggregation part of the query
ColumnTypeAggregation = ColumnType{valuer.NewString("aggregation")}
)
type ColumnDescriptor struct {
telemetrytypes.TelemetryFieldKey
QueryName string `json:"queryName"`
AggregationIndex int64 `json:"aggregationIndex"`
Type ColumnType `json:"columnType"`
}
type ScalarData struct {
Columns []ColumnDescriptor `json:"columns"`
Data [][]any `json:"data"`
}
type RawData struct {
QueryName string `json:"queryName"`
Rows []RawRow `json:"rows"`
}
type RawRow struct {
Timestamp time.Time `json:"timestamp"`
Data map[string]any `json:"data"`
}