mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
## 📄 Summary
To reliably migrate the alerts and dashboards, we need access to the telemetrystore to fetch some metadata and while doing migration, I need to log some stuff to fix stuff later.
Key changes:
- Modified the migration to include telemetrystore and a logging provider (open to using a standard logger instead)
- To avoid the previous issues with imported dashboards failing during migration, I've ensured that imported JSON files are automatically transformed when migration is active
- Implemented detailed logic to handle dashboard migration cleanly and prevent unnecessary errors
- Separated the core migration logic from SQL migration code, as users from the dot metrics migration requested shareable code snippets for local migrations. This modular approach allows others to easily reuse the migration functionality.
Known: I didn't register the migration yet in this PR, and will not merge this yet, so please review with that in mid.
808 lines
25 KiB
Go
808 lines
25 KiB
Go
package telemetrytraces
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/factory"
|
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
"github.com/huandu/go-sqlbuilder"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
var (
|
|
ErrUnsupportedAggregation = errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported aggregation")
|
|
)
|
|
|
|
type traceQueryStatementBuilder struct {
|
|
logger *slog.Logger
|
|
metadataStore telemetrytypes.MetadataStore
|
|
fm qbtypes.FieldMapper
|
|
cb qbtypes.ConditionBuilder
|
|
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation]
|
|
aggExprRewriter qbtypes.AggExprRewriter
|
|
telemetryStore telemetrystore.TelemetryStore
|
|
}
|
|
|
|
var _ qbtypes.StatementBuilder[qbtypes.TraceAggregation] = (*traceQueryStatementBuilder)(nil)
|
|
|
|
func NewTraceQueryStatementBuilder(
|
|
settings factory.ProviderSettings,
|
|
metadataStore telemetrytypes.MetadataStore,
|
|
fieldMapper qbtypes.FieldMapper,
|
|
conditionBuilder qbtypes.ConditionBuilder,
|
|
resourceFilterStmtBuilder qbtypes.StatementBuilder[qbtypes.TraceAggregation],
|
|
aggExprRewriter qbtypes.AggExprRewriter,
|
|
telemetryStore telemetrystore.TelemetryStore,
|
|
) *traceQueryStatementBuilder {
|
|
tracesSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetrytraces")
|
|
return &traceQueryStatementBuilder{
|
|
logger: tracesSettings.Logger(),
|
|
metadataStore: metadataStore,
|
|
fm: fieldMapper,
|
|
cb: conditionBuilder,
|
|
resourceFilterStmtBuilder: resourceFilterStmtBuilder,
|
|
aggExprRewriter: aggExprRewriter,
|
|
telemetryStore: telemetryStore,
|
|
}
|
|
}
|
|
|
|
// Build builds a SQL query for traces based on the given parameters
|
|
func (b *traceQueryStatementBuilder) Build(
|
|
ctx context.Context,
|
|
start uint64,
|
|
end uint64,
|
|
requestType qbtypes.RequestType,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
start = querybuilder.ToNanoSecs(start)
|
|
end = querybuilder.ToNanoSecs(end)
|
|
|
|
keySelectors := getKeySelectors(query)
|
|
|
|
keys, _, err := b.metadataStore.GetKeysMulti(ctx, keySelectors)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.adjustKeys(ctx, keys, query)
|
|
|
|
// Check if filter contains trace_id(s) and optimize time range if needed
|
|
if query.Filter != nil && query.Filter.Expression != "" && b.telemetryStore != nil {
|
|
traceIDs, found := ExtractTraceIDsFromFilter(query.Filter.Expression)
|
|
if found && len(traceIDs) > 0 {
|
|
finder := NewTraceTimeRangeFinder(b.telemetryStore)
|
|
|
|
traceStart, traceEnd, err := finder.GetTraceTimeRangeMulti(ctx, traceIDs)
|
|
if err != nil {
|
|
b.logger.DebugContext(ctx, "failed to get trace time range", "trace_ids", traceIDs, "error", err)
|
|
} else if traceStart > 0 && traceEnd > 0 {
|
|
start = uint64(traceStart)
|
|
end = uint64(traceEnd)
|
|
b.logger.DebugContext(ctx, "optimized time range for traces", "trace_ids", traceIDs, "start", start, "end", end)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create SQL builder
|
|
q := sqlbuilder.NewSelectBuilder()
|
|
|
|
switch requestType {
|
|
case qbtypes.RequestTypeRaw:
|
|
return b.buildListQuery(ctx, q, query, start, end, keys, variables)
|
|
case qbtypes.RequestTypeTimeSeries:
|
|
return b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
|
|
case qbtypes.RequestTypeScalar:
|
|
return b.buildScalarQuery(ctx, q, query, start, end, keys, variables, false, false)
|
|
case qbtypes.RequestTypeTrace:
|
|
return b.buildTraceQuery(ctx, q, query, start, end, keys, variables)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported request type: %s", requestType)
|
|
}
|
|
|
|
func getKeySelectors(query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) []*telemetrytypes.FieldKeySelector {
|
|
var keySelectors []*telemetrytypes.FieldKeySelector
|
|
|
|
for idx := range query.Aggregations {
|
|
aggExpr := query.Aggregations[idx]
|
|
selectors := querybuilder.QueryStringToKeysSelectors(aggExpr.Expression)
|
|
keySelectors = append(keySelectors, selectors...)
|
|
}
|
|
|
|
if query.Filter != nil && query.Filter.Expression != "" {
|
|
whereClauseSelectors := querybuilder.QueryStringToKeysSelectors(query.Filter.Expression)
|
|
keySelectors = append(keySelectors, whereClauseSelectors...)
|
|
}
|
|
|
|
for idx := range query.GroupBy {
|
|
groupBy := query.GroupBy[idx]
|
|
selectors := querybuilder.QueryStringToKeysSelectors(groupBy.TelemetryFieldKey.Name)
|
|
keySelectors = append(keySelectors, selectors...)
|
|
}
|
|
|
|
for idx := range query.SelectFields {
|
|
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
|
Name: query.SelectFields[idx].Name,
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: query.SelectFields[idx].FieldContext,
|
|
})
|
|
}
|
|
|
|
for idx := range query.Order {
|
|
keySelectors = append(keySelectors, &telemetrytypes.FieldKeySelector{
|
|
Name: query.Order[idx].Key.Name,
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: query.Order[idx].Key.FieldContext,
|
|
})
|
|
}
|
|
|
|
for idx := range keySelectors {
|
|
keySelectors[idx].Signal = telemetrytypes.SignalTraces
|
|
keySelectors[idx].SelectorMatchType = telemetrytypes.FieldSelectorMatchTypeExact
|
|
}
|
|
|
|
return keySelectors
|
|
}
|
|
|
|
func (b *traceQueryStatementBuilder) adjustKeys(ctx context.Context, keys map[string][]*telemetrytypes.TelemetryFieldKey, query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) {
|
|
// for group by / order by / selected fields, if there is a key
|
|
// that exactly matches the name of intrinsic / calculated field but has
|
|
// a field context or data type that doesn't match the field context or data type of the
|
|
// intrinsic field,
|
|
// and there is no additional key present in the data with the incoming key match,
|
|
// then override the given context with
|
|
// intrinsic / calculated field context and data type
|
|
// Why does that happen? Because we have a lot of assets created by users and shared over web
|
|
// that has incorrect context or data type populated so we fix it
|
|
// note: this override happens only when there is no match; if there is a match,
|
|
// we can't make decision on behalf of users so we let it use unmodified
|
|
|
|
// example: {"key": "httpRoute","type": "tag","dataType": "string"}
|
|
// This is sent as "tag", when it's not, this was earlier managed with
|
|
// `isColumn`, which we don't have in v5 (because it's not a user concern whether it's mat col or not)
|
|
// Such requests as-is look for attributes, the following code exists to handle them
|
|
checkMatch := func(k *telemetrytypes.TelemetryFieldKey) {
|
|
var overallMatch bool
|
|
|
|
findMatch := func(staticKeys map[string]telemetrytypes.TelemetryFieldKey) bool {
|
|
// for a given key `k`, iterate over the metadata keys `keys`
|
|
// and see if there is any exact match
|
|
match := false
|
|
for _, mapKey := range keys[k.Name] {
|
|
if mapKey.FieldContext == k.FieldContext && mapKey.FieldDataType == k.FieldDataType {
|
|
match = true
|
|
}
|
|
}
|
|
// we don't have exact match, then it's doesn't exist in attribute or resource attribute
|
|
// use the intrinsic/calculated field
|
|
if !match {
|
|
b.logger.InfoContext(ctx, "overriding the field context and data type", "key", k.Name)
|
|
k.FieldContext = staticKeys[k.Name].FieldContext
|
|
k.FieldDataType = staticKeys[k.Name].FieldDataType
|
|
}
|
|
return match
|
|
}
|
|
|
|
if _, ok := IntrinsicFields[k.Name]; ok {
|
|
overallMatch = overallMatch || findMatch(IntrinsicFields)
|
|
}
|
|
if _, ok := CalculatedFields[k.Name]; ok {
|
|
overallMatch = overallMatch || findMatch(CalculatedFields)
|
|
}
|
|
if _, ok := IntrinsicFieldsDeprecated[k.Name]; ok {
|
|
overallMatch = overallMatch || findMatch(IntrinsicFieldsDeprecated)
|
|
}
|
|
if _, ok := CalculatedFieldsDeprecated[k.Name]; ok {
|
|
overallMatch = overallMatch || findMatch(CalculatedFieldsDeprecated)
|
|
}
|
|
|
|
if !overallMatch {
|
|
// check if all the key for the given field have been materialized, if so
|
|
// set the key to materialized
|
|
materilized := true
|
|
for _, key := range keys[k.Name] {
|
|
materilized = materilized && key.Materialized
|
|
}
|
|
k.Materialized = materilized
|
|
}
|
|
}
|
|
|
|
for idx := range query.GroupBy {
|
|
checkMatch(&query.GroupBy[idx].TelemetryFieldKey)
|
|
}
|
|
for idx := range query.Order {
|
|
checkMatch(&query.Order[idx].Key.TelemetryFieldKey)
|
|
}
|
|
for idx := range query.SelectFields {
|
|
checkMatch(&query.SelectFields[idx])
|
|
}
|
|
|
|
// add deprecated fields only during statement building
|
|
// why?
|
|
// 1. to not fail filter expression that use deprecated cols
|
|
// 2. this could have been moved to metadata fetching itself, however, that
|
|
// would mean, they also show up in suggestions we we don't want to do
|
|
for fieldKeyName, fieldKey := range IntrinsicFieldsDeprecated {
|
|
if _, ok := keys[fieldKeyName]; !ok {
|
|
keys[fieldKeyName] = []*telemetrytypes.TelemetryFieldKey{&fieldKey}
|
|
} else {
|
|
keys[fieldKeyName] = append(keys[fieldKeyName], &fieldKey)
|
|
}
|
|
}
|
|
for fieldKeyName, fieldKey := range CalculatedFieldsDeprecated {
|
|
if _, ok := keys[fieldKeyName]; !ok {
|
|
keys[fieldKeyName] = []*telemetrytypes.TelemetryFieldKey{&fieldKey}
|
|
} else {
|
|
keys[fieldKeyName] = append(keys[fieldKeyName], &fieldKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildListQuery builds a query for list panel type
|
|
func (b *traceQueryStatementBuilder) buildListQuery(
|
|
ctx context.Context,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
var (
|
|
cteFragments []string
|
|
cteArgs [][]any
|
|
)
|
|
|
|
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
|
return nil, err
|
|
} else if frag != "" {
|
|
cteFragments = append(cteFragments, frag)
|
|
cteArgs = append(cteArgs, args)
|
|
}
|
|
|
|
selectedFields := query.SelectFields
|
|
|
|
if len(selectedFields) == 0 {
|
|
sortedKeys := maps.Keys(DefaultFields)
|
|
slices.Sort(sortedKeys)
|
|
for _, key := range sortedKeys {
|
|
selectedFields = append(selectedFields, DefaultFields[key])
|
|
}
|
|
}
|
|
|
|
selectFieldKeys := []string{}
|
|
for _, field := range selectedFields {
|
|
selectFieldKeys = append(selectFieldKeys, field.Name)
|
|
}
|
|
|
|
for _, x := range []string{"timestamp", "span_id", "trace_id"} {
|
|
if !slices.Contains(selectFieldKeys, x) {
|
|
selectedFields = append(selectedFields, DefaultFields[x])
|
|
}
|
|
}
|
|
|
|
// TODO: should we deprecate `SelectFields` and return everything from a span like we do for logs?
|
|
for _, field := range selectedFields {
|
|
colExpr, err := b.fm.ColumnExpressionFor(ctx, &field, keys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sb.SelectMore(colExpr)
|
|
}
|
|
|
|
// From table
|
|
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
|
|
|
// Add filter conditions
|
|
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Add order by
|
|
for _, orderBy := range query.Order {
|
|
colExpr, err := b.fm.ColumnExpressionFor(ctx, &orderBy.Key.TelemetryFieldKey, keys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sb.OrderBy(fmt.Sprintf("%s %s", colExpr, orderBy.Direction.StringValue()))
|
|
}
|
|
|
|
// Add limit and offset
|
|
if query.Limit > 0 {
|
|
sb.Limit(query.Limit)
|
|
} else {
|
|
sb.Limit(100)
|
|
}
|
|
|
|
if query.Offset > 0 {
|
|
sb.Offset(query.Offset)
|
|
}
|
|
|
|
mainSQL, mainArgs := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
|
|
|
finalSQL := querybuilder.CombineCTEs(cteFragments) + mainSQL
|
|
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
|
|
|
stmt := &qbtypes.Statement{
|
|
Query: finalSQL,
|
|
Args: finalArgs,
|
|
}
|
|
if preparedWhereClause != nil {
|
|
stmt.Warnings = preparedWhereClause.Warnings
|
|
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
|
}
|
|
|
|
return stmt, nil
|
|
}
|
|
|
|
func (b *traceQueryStatementBuilder) buildTraceQuery(
|
|
ctx context.Context,
|
|
_ *sqlbuilder.SelectBuilder,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
startBucket := start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
|
|
endBucket := end / querybuilder.NsToSeconds
|
|
|
|
distSB := sqlbuilder.NewSelectBuilder()
|
|
distSB.Select("trace_id")
|
|
distSB.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
|
|
|
var (
|
|
cteFragments []string
|
|
cteArgs [][]any
|
|
)
|
|
|
|
if frag, args, err := b.maybeAttachResourceFilter(ctx, distSB, query, start, end, variables); err != nil {
|
|
return nil, err
|
|
} else if frag != "" {
|
|
cteFragments = append(cteFragments, frag)
|
|
cteArgs = append(cteArgs, args)
|
|
}
|
|
|
|
// Add filter conditions
|
|
preparedWhereClause, err := b.addFilterCondition(ctx, distSB, start, end, query, keys, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
distSQL, distArgs := distSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
|
|
|
cteFragments = append(cteFragments, fmt.Sprintf("__toe AS (%s)", distSQL))
|
|
cteArgs = append(cteArgs, distArgs)
|
|
|
|
// Build the inner subquery for root spans
|
|
innerSB := sqlbuilder.NewSelectBuilder()
|
|
innerSB.Select("trace_id", "duration_nano", sqlbuilder.Escape("resource_string_service$$name as `service.name`"), "name")
|
|
innerSB.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
|
innerSB.Where("parent_span_id = ''")
|
|
|
|
// Add time filter to inner query
|
|
innerSB.Where(
|
|
innerSB.GE("timestamp", fmt.Sprintf("%d", start)),
|
|
innerSB.L("timestamp", fmt.Sprintf("%d", end)),
|
|
innerSB.GE("ts_bucket_start", startBucket),
|
|
innerSB.LE("ts_bucket_start", endBucket))
|
|
|
|
// order by duration and limit 1 per trace
|
|
innerSB.OrderBy("duration_nano DESC")
|
|
innerSB.SQL("LIMIT 1 BY trace_id")
|
|
|
|
innerSQL, innerArgs := innerSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
|
|
|
cteFragments = append(cteFragments, fmt.Sprintf("__toe_duration_sorted AS (%s)", innerSQL))
|
|
cteArgs = append(cteArgs, innerArgs)
|
|
|
|
// main query that joins everything
|
|
mainSB := sqlbuilder.NewSelectBuilder()
|
|
mainSB.Select(
|
|
"__toe_duration_sorted.`service.name` AS `service.name`",
|
|
"__toe_duration_sorted.name AS `name`",
|
|
"count() AS span_count",
|
|
"__toe_duration_sorted.duration_nano AS `duration_nano`",
|
|
"__toe_duration_sorted.trace_id AS `trace_id`",
|
|
)
|
|
|
|
// Join the distributed table with the inner subquery
|
|
mainSB.SQL("FROM __toe")
|
|
mainSB.SQL("INNER JOIN __toe_duration_sorted")
|
|
mainSB.SQL("ON __toe.trace_id = __toe_duration_sorted.trace_id")
|
|
|
|
// Group by trace-level fields
|
|
mainSB.GroupBy("trace_id", "duration_nano", "name", "`service.name`")
|
|
|
|
// order by duration only supported for now
|
|
mainSB.OrderBy("duration_nano DESC")
|
|
|
|
// Limit by trace_id to ensure one row per trace
|
|
mainSB.SQL("LIMIT 1 BY trace_id")
|
|
|
|
if query.Limit > 0 {
|
|
mainSB.Limit(query.Limit)
|
|
} else {
|
|
mainSB.Limit(100)
|
|
}
|
|
|
|
if query.Offset > 0 {
|
|
mainSB.Offset(query.Offset)
|
|
}
|
|
|
|
mainSQL, mainArgs := mainSB.BuildWithFlavor(sqlbuilder.ClickHouse)
|
|
|
|
// combine it all together: WITH … SELECT …
|
|
finalSQL := querybuilder.CombineCTEs(cteFragments) + mainSQL + " SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000"
|
|
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
|
|
|
stmt := &qbtypes.Statement{
|
|
Query: finalSQL,
|
|
Args: finalArgs,
|
|
}
|
|
if preparedWhereClause != nil {
|
|
stmt.Warnings = preparedWhereClause.Warnings
|
|
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
|
}
|
|
|
|
return stmt, nil
|
|
}
|
|
|
|
func (b *traceQueryStatementBuilder) buildTimeSeriesQuery(
|
|
ctx context.Context,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
var (
|
|
cteFragments []string
|
|
cteArgs [][]any
|
|
)
|
|
|
|
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
|
return nil, err
|
|
} else if frag != "" {
|
|
cteFragments = append(cteFragments, frag)
|
|
cteArgs = append(cteArgs, args)
|
|
}
|
|
|
|
sb.SelectMore(fmt.Sprintf(
|
|
"toStartOfInterval(timestamp, INTERVAL %d SECOND) AS ts",
|
|
int64(query.StepInterval.Seconds()),
|
|
))
|
|
|
|
var allGroupByArgs []any
|
|
|
|
// Keep original column expressions so we can build the tuple
|
|
fieldNames := make([]string, 0, len(query.GroupBy))
|
|
for _, gb := range query.GroupBy {
|
|
expr, args, err := querybuilder.CollisionHandledFinalExpr(ctx, &gb.TelemetryFieldKey, b.fm, b.cb, keys, telemetrytypes.FieldDataTypeString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
|
|
allGroupByArgs = append(allGroupByArgs, args...)
|
|
sb.SelectMore(colExpr)
|
|
fieldNames = append(fieldNames, fmt.Sprintf("`%s`", gb.TelemetryFieldKey.Name))
|
|
}
|
|
|
|
// Aggregations
|
|
allAggChArgs := make([]any, 0)
|
|
for i, agg := range query.Aggregations {
|
|
rewritten, chArgs, err := b.aggExprRewriter.Rewrite(
|
|
ctx, agg.Expression,
|
|
uint64(query.StepInterval.Seconds()),
|
|
keys,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allAggChArgs = append(allAggChArgs, chArgs...)
|
|
sb.SelectMore(fmt.Sprintf("%s AS __result_%d", rewritten, i))
|
|
}
|
|
|
|
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
|
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var finalSQL string
|
|
var finalArgs []any
|
|
|
|
if query.Limit > 0 && len(query.GroupBy) > 0 {
|
|
// build the scalar “top/bottom-N” query in its own builder.
|
|
cteSB := sqlbuilder.NewSelectBuilder()
|
|
cteStmt, err := b.buildScalarQuery(ctx, cteSB, query, start, end, keys, variables, true, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cteFragments = append(cteFragments, fmt.Sprintf("__limit_cte AS (%s)", cteStmt.Query))
|
|
cteArgs = append(cteArgs, cteStmt.Args)
|
|
|
|
// Constrain the main query to the rows that appear in the CTE.
|
|
tuple := fmt.Sprintf("(%s)", strings.Join(fieldNames, ", "))
|
|
sb.Where(fmt.Sprintf("%s GLOBAL IN (SELECT %s FROM __limit_cte)", tuple, strings.Join(fieldNames, ", ")))
|
|
|
|
// Group by all dimensions
|
|
sb.GroupBy("ts")
|
|
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
|
if query.Having != nil && query.Having.Expression != "" {
|
|
rewriter := querybuilder.NewHavingExpressionRewriter()
|
|
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
|
sb.Having(rewrittenExpr)
|
|
}
|
|
|
|
if len(query.Order) != 0 {
|
|
for _, orderBy := range query.Order {
|
|
_, ok := aggOrderBy(orderBy, query)
|
|
if !ok {
|
|
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
|
}
|
|
}
|
|
sb.OrderBy("ts desc")
|
|
}
|
|
|
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
|
mainSQL, mainArgs := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
|
|
|
// Stitch it all together: WITH … SELECT …
|
|
finalSQL = querybuilder.CombineCTEs(cteFragments) + mainSQL
|
|
finalArgs = querybuilder.PrependArgs(cteArgs, mainArgs)
|
|
|
|
} else {
|
|
sb.GroupBy("ts")
|
|
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
|
if query.Having != nil && query.Having.Expression != "" {
|
|
rewriter := querybuilder.NewHavingExpressionRewriter()
|
|
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
|
sb.Having(rewrittenExpr)
|
|
}
|
|
|
|
if len(query.Order) != 0 {
|
|
for _, orderBy := range query.Order {
|
|
_, ok := aggOrderBy(orderBy, query)
|
|
if !ok {
|
|
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
|
}
|
|
}
|
|
sb.OrderBy("ts desc")
|
|
}
|
|
|
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
|
mainSQL, mainArgs := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
|
|
|
// Stitch it all together: WITH … SELECT …
|
|
finalSQL = querybuilder.CombineCTEs(cteFragments) + mainSQL
|
|
finalArgs = querybuilder.PrependArgs(cteArgs, mainArgs)
|
|
}
|
|
|
|
stmt := &qbtypes.Statement{
|
|
Query: finalSQL,
|
|
Args: finalArgs,
|
|
}
|
|
if preparedWhereClause != nil {
|
|
stmt.Warnings = preparedWhereClause.Warnings
|
|
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
|
}
|
|
|
|
return stmt, nil
|
|
}
|
|
|
|
// buildScalarQuery builds a query for scalar panel type
|
|
func (b *traceQueryStatementBuilder) buildScalarQuery(
|
|
ctx context.Context,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
variables map[string]qbtypes.VariableItem,
|
|
skipResourceCTE bool,
|
|
skipHaving bool,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
var (
|
|
cteFragments []string
|
|
cteArgs [][]any
|
|
)
|
|
|
|
if frag, args, err := b.maybeAttachResourceFilter(ctx, sb, query, start, end, variables); err != nil {
|
|
return nil, err
|
|
} else if frag != "" && !skipResourceCTE {
|
|
cteFragments = append(cteFragments, frag)
|
|
cteArgs = append(cteArgs, args)
|
|
}
|
|
|
|
allAggChArgs := []any{}
|
|
|
|
var allGroupByArgs []any
|
|
for _, gb := range query.GroupBy {
|
|
expr, args, err := querybuilder.CollisionHandledFinalExpr(ctx, &gb.TelemetryFieldKey, b.fm, b.cb, keys, telemetrytypes.FieldDataTypeString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
colExpr := fmt.Sprintf("toString(%s) AS `%s`", expr, gb.TelemetryFieldKey.Name)
|
|
allGroupByArgs = append(allGroupByArgs, args...)
|
|
sb.SelectMore(colExpr)
|
|
}
|
|
|
|
// for scalar queries, the rate would be end-start
|
|
rateInterval := (end - start) / querybuilder.NsToSeconds
|
|
|
|
// Add aggregation
|
|
if len(query.Aggregations) > 0 {
|
|
for idx := range query.Aggregations {
|
|
aggExpr := query.Aggregations[idx]
|
|
rewritten, chArgs, err := b.aggExprRewriter.Rewrite(
|
|
ctx, aggExpr.Expression,
|
|
rateInterval,
|
|
keys,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allAggChArgs = append(allAggChArgs, chArgs...)
|
|
sb.SelectMore(fmt.Sprintf("%s AS __result_%d", rewritten, idx))
|
|
}
|
|
}
|
|
|
|
// From table
|
|
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
|
|
|
|
// Add filter conditions
|
|
preparedWhereClause, err := b.addFilterCondition(ctx, sb, start, end, query, keys, variables)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group by dimensions
|
|
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
|
|
|
// Add having clause if needed
|
|
if query.Having != nil && query.Having.Expression != "" && !skipHaving {
|
|
rewriter := querybuilder.NewHavingExpressionRewriter()
|
|
rewrittenExpr := rewriter.RewriteForTraces(query.Having.Expression, query.Aggregations)
|
|
sb.Having(rewrittenExpr)
|
|
}
|
|
|
|
// Add order by
|
|
for _, orderBy := range query.Order {
|
|
idx, ok := aggOrderBy(orderBy, query)
|
|
if ok {
|
|
sb.OrderBy(fmt.Sprintf("__result_%d %s", idx, orderBy.Direction.StringValue()))
|
|
} else {
|
|
sb.OrderBy(fmt.Sprintf("`%s` %s", orderBy.Key.Name, orderBy.Direction.StringValue()))
|
|
}
|
|
}
|
|
|
|
// if there is no order by, then use the __result_0 as the order by
|
|
if len(query.Order) == 0 {
|
|
sb.OrderBy("__result_0 DESC")
|
|
}
|
|
|
|
// Add limit and offset
|
|
if query.Limit > 0 {
|
|
sb.Limit(query.Limit)
|
|
}
|
|
|
|
combinedArgs := append(allGroupByArgs, allAggChArgs...)
|
|
|
|
mainSQL, mainArgs := sb.BuildWithFlavor(sqlbuilder.ClickHouse, combinedArgs...)
|
|
|
|
finalSQL := querybuilder.CombineCTEs(cteFragments) + mainSQL
|
|
finalArgs := querybuilder.PrependArgs(cteArgs, mainArgs)
|
|
|
|
stmt := &qbtypes.Statement{
|
|
Query: finalSQL,
|
|
Args: finalArgs,
|
|
}
|
|
if preparedWhereClause != nil {
|
|
stmt.Warnings = preparedWhereClause.Warnings
|
|
stmt.WarningsDocURL = preparedWhereClause.WarningsDocURL
|
|
}
|
|
|
|
return stmt, nil
|
|
}
|
|
|
|
// buildFilterCondition builds SQL condition from filter expression
|
|
func (b *traceQueryStatementBuilder) addFilterCondition(
|
|
_ context.Context,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
start, end uint64,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
keys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*querybuilder.PreparedWhereClause, error) {
|
|
|
|
var preparedWhereClause *querybuilder.PreparedWhereClause
|
|
var err error
|
|
|
|
if query.Filter != nil && query.Filter.Expression != "" {
|
|
// add filter expression
|
|
preparedWhereClause, err = querybuilder.PrepareWhereClause(query.Filter.Expression, querybuilder.FilterExprVisitorOpts{
|
|
FieldMapper: b.fm,
|
|
ConditionBuilder: b.cb,
|
|
FieldKeys: keys,
|
|
SkipResourceFilter: true,
|
|
Variables: variables,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if preparedWhereClause != nil {
|
|
sb.AddWhereClause(preparedWhereClause.WhereClause)
|
|
}
|
|
|
|
// add time filter
|
|
startBucket := start/querybuilder.NsToSeconds - querybuilder.BucketAdjustment
|
|
endBucket := end / querybuilder.NsToSeconds
|
|
|
|
sb.Where(sb.GE("timestamp", fmt.Sprintf("%d", start)), sb.L("timestamp", fmt.Sprintf("%d", end)), sb.GE("ts_bucket_start", startBucket), sb.LE("ts_bucket_start", endBucket))
|
|
|
|
return preparedWhereClause, nil
|
|
}
|
|
|
|
func aggOrderBy(k qbtypes.OrderBy, q qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) (int, bool) {
|
|
for i, agg := range q.Aggregations {
|
|
if k.Key.Name == agg.Alias ||
|
|
k.Key.Name == agg.Expression ||
|
|
k.Key.Name == fmt.Sprintf("%d", i) {
|
|
return i, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func (b *traceQueryStatementBuilder) maybeAttachResourceFilter(
|
|
ctx context.Context,
|
|
sb *sqlbuilder.SelectBuilder,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (cteSQL string, cteArgs []any, err error) {
|
|
|
|
stmt, err := b.buildResourceFilterCTE(ctx, query, start, end, variables)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
|
|
|
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, nil
|
|
}
|
|
|
|
func (b *traceQueryStatementBuilder) buildResourceFilterCTE(
|
|
ctx context.Context,
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation],
|
|
start, end uint64,
|
|
variables map[string]qbtypes.VariableItem,
|
|
) (*qbtypes.Statement, error) {
|
|
|
|
return b.resourceFilterStmtBuilder.Build(
|
|
ctx,
|
|
start,
|
|
end,
|
|
qbtypes.RequestTypeRaw,
|
|
query,
|
|
variables,
|
|
)
|
|
}
|