mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The v5 logs and traces statement builders read event.log and event.span, where every row carries its org. Each read now binds `org = ?` to the tenant: the validated X-Org-Id for a request (set by the Hanzo IAM identity), or the rule's org name for a scheduled rule that reads logs or traces. A read with no tenant is refused as forbidden instead of reading every org. This covers list, time series, scalar and trace requests and both span reads of the trace operator. Co-authored-by: Hanzo Dev <[email protected]>
1702 lines
88 KiB
Go
1702 lines
88 KiB
Go
package telemetrytraces
|
|
|
|
import (
|
|
"regexp"
|
|
"testing"
|
|
"time"
|
|
|
|
dsmock "github.com/hanzo-ds/mock"
|
|
"github.com/hanzoai/o11y/pkg/errors"
|
|
"github.com/hanzoai/o11y/pkg/flagger/flaggertest"
|
|
"github.com/hanzoai/o11y/pkg/instrumentation/instrumentationtest"
|
|
"github.com/hanzoai/o11y/pkg/querybuilder"
|
|
"github.com/hanzoai/o11y/pkg/telemetrystore"
|
|
"github.com/hanzoai/o11y/pkg/telemetrystore/telemetrystoretest"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
|
|
"github.com/hanzoai/o11y/pkg/types/telemetrytypes/telemetrytypestest"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type regexQueryMatcher struct{}
|
|
|
|
func (m *regexQueryMatcher) Match(expectedSQL, actualSQL string) error {
|
|
re, err := regexp.Compile(expectedSQL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !re.MatchString(actualSQL) {
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "expected query to match %s, got %s", expectedSQL, actualSQL)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestStatementBuilder(t *testing.T) {
|
|
// releaseTime is chosen so it lands inside the standard [1747947419000, 1747983448000]ms
|
|
// test window, keeping the multiIf SQL form for resource fields.
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
requestType qbtypes.RequestType
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
expected qbtypes.Statement
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "test",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "OR b/w resource attr and attribute",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual' OR http.request.method = 'GET'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE (service = ? OR (attributes['http.request.method'] = ? AND mapContains(attributes, 'http.request.method') = ?)) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE (service = ? OR (attributes['http.request.method'] = ? AND mapContains(attributes, 'http.request.method') = ?)) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "OR with regexp",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "materialized.key.name REGEXP 'redis-manual' OR service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE ((match(attributes['materialized.key.name'], ?) AND mapContains(attributes, 'materialized.key.name') = ?) OR service = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE ((match(attributes['materialized.key.name'], ?) AND mapContains(attributes, 'materialized.key.name') = ?) OR service = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "legacy httpRoute in group by",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "httpRoute",
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(attributes['http.route'] <> ?, attributes['http.route'], NULL)) AS `httpRoute`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `httpRoute` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(attributes['http.route'] <> ?, attributes['http.route'], NULL)) AS `httpRoute`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`httpRoute`) GLOBAL IN (SELECT `httpRoute` FROM __limit_cte) GROUP BY ts, `httpRoute`",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "legacy fields in search and group by",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "serviceName = $service.name AND httpMethod EXISTS AND spanKind = 'Server'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "httpRoute",
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
},
|
|
},
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "httpMethod",
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(attributes['http.route'] <> ?, attributes['http.route'], NULL)) AS `httpRoute`, toString(multiIf(if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) <> ?, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']), NULL)) AS `httpMethod`, count() AS __result_0 FROM event.span WHERE (service = ? AND if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) <> ? AND kind = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `httpRoute`, `httpMethod` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(attributes['http.route'] <> ?, attributes['http.route'], NULL)) AS `httpRoute`, toString(multiIf(if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) <> ?, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']), NULL)) AS `httpMethod`, count() AS __result_0 FROM event.span WHERE (service = ? AND if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) <> ? AND kind = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`httpRoute`, `httpMethod`) GLOBAL IN (SELECT `httpRoute`, `httpMethod` FROM __limit_cte) GROUP BY ts, `httpRoute`, `httpMethod`",
|
|
Args: []any{"", "", "redis-manual", "", "Server", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "", "", "redis-manual", "", "Server", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "context as key prefix test",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "sum(metric.max_count)",
|
|
Alias: "metric.max_count",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "max_count",
|
|
FieldContext: telemetrytypes.FieldContextMetric,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'metric.max_count') = ?, toFloat64(toFloat64OrNull(attributes['metric.max_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 desc LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'metric.max_count') = ?, toFloat64(toFloat64OrNull(attributes['metric.max_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY ts desc",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "mat number key in aggregation test",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "sum(cart.items_count)",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'cart.items_count') = ?, toFloat64(toFloat64OrNull(attributes['cart.items_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'cart.items_count') = ?, toFloat64(toFloat64OrNull(attributes['cart.items_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "mat number key in aggregation test with order by service",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "sum(cart.items_count)",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'cart.items_count') = ?, toFloat64(toFloat64OrNull(attributes['cart.items_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY `service.name` desc LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, sum(multiIf(mapContains(attributes, 'cart.items_count') = ?, toFloat64(toFloat64OrNull(attributes['cart.items_count'])), NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name` ORDER BY `service.name` desc, ts desc",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "Legacy column with incorrect field context test",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "count()",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "responseStatusCode",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) <> ?, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']), NULL)) AS `responseStatusCode`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) <> ?, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']), NULL)) AS `responseStatusCode`, count() AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "Legacy column in aggregation and incorrect field context test",
|
|
requestType: qbtypes.RequestTypeTimeSeries,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "p90(durationNano)",
|
|
},
|
|
},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "responseStatusCode",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __limit_cte AS (SELECT toString(multiIf(if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) <> ?, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']), NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration <> ?, duration, NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `responseStatusCode` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) <> ?, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']), NULL)) AS `responseStatusCode`, quantile(0.90)(multiIf(duration <> ?, duration, NULL)) AS __result_0 FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`responseStatusCode`) GLOBAL IN (SELECT `responseStatusCode` FROM __limit_cte) GROUP BY ts, `responseStatusCode`",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "", 0, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "", 0, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
statementBuilder := NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
nil,
|
|
fl,
|
|
false,
|
|
100000,
|
|
)
|
|
|
|
vars := map[string]qbtypes.VariableItem{
|
|
"service.name": {
|
|
Value: "redis-manual",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
q, err := statementBuilder.Build(tenantCtx(), 1747947419000, 1747983448000, c.requestType, c.query, vars)
|
|
|
|
if c.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expected.Query, q.Query)
|
|
require.Equal(t, c.expected.Args, q.Args)
|
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatementBuilderListQuery(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
requestType qbtypes.RequestType
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
expected qbtypes.Statement
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "List query with mat selected fields",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "service.name",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: true,
|
|
},
|
|
{
|
|
Name: "duration_nano",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
},
|
|
{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
|
},
|
|
},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, service AS `service.name`, duration AS `duration_nano`, toFloat64OrNull(attributes['cart.items_count']) AS `cart.items_count` FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with default fields and attribute order by",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, attributes['trace_state'] AS `trace_state`, parent AS `parent_span_id`, toUInt32OrZero(attributes['flags']) AS `flags`, name AS `name`, toInt8(multiIf(kind = 'internal', 1, kind = 'server', 2, kind = 'client', 3, kind = 'producer', 4, kind = 'consumer', 5, 0)) AS `kind`, kind AS `kind_string`, duration AS `duration_nano`, toInt16(multiIf(status = 'error', 2, status = 'ok', 1, 0)) AS `status_code`, attributes['status.message'] AS `status_message`, status AS `status_code_string`, JSONExtractArrayRaw(attributes['span.events']) AS `events`, attributes['links'] AS `links`, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) AS `response_status_code`, url AS `external_http_url`, url AS `http_url`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `external_http_method`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `http_method`, host AS `http_host`, attributes['db.name'] AS `db_name`, attributes['db.operation'] AS `db_operation`, toBool(status = 'error') AS `has_error`, attributes['is_remote'] AS `is_remote`, attributes AS attributes_string, CAST(map() AS Map(String, Float64)) AS attributes_number, CAST(map() AS Map(String, Bool)) AS attributes_bool, map('service.name', toString(service), 'host', toString(host)) AS resources_string FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY attributes['user.id'] AS `user.id` desc LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with legacy fields",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "serviceName",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "durationNano",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
},
|
|
{
|
|
Name: "httpMethod",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "responseStatusCode",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, service AS `serviceName`, duration AS `durationNano`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `httpMethod`, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) AS `responseStatusCode` FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with legacy fields with mixed materialization field",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "serviceName",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "durationNano",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
},
|
|
{
|
|
Name: "httpMethod",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
// "mixed.materialization.key" exists in both attribute and resource,
|
|
// attribute being materialized and resource being non-materialized.
|
|
Name: "mixed.materialization.key",
|
|
},
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, service AS `serviceName`, duration AS `durationNano`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `httpMethod`, multiIf(toString(attributes['mixed.materialization.key']) != '', toString(attributes['mixed.materialization.key']), toString(attributes['mixed.materialization.key']) != '', toString(attributes['mixed.materialization.key']), NULL) AS `mixed.materialization.key` FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with legacy fields with mixed materialization field with context provided",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "serviceName",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "durationNano",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
},
|
|
{
|
|
Name: "httpMethod",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
// "mixed.materialization.key" exists in both attribute and resource,
|
|
// attribute being materialized and resource being non-materialized.
|
|
Name: "mixed.materialization.key",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
},
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, service AS `serviceName`, duration AS `durationNano`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `httpMethod`, attributes['mixed.materialization.key'] AS `mixed.materialization.key` FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with legacy fields with field that doesn't exist",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "serviceName",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "durationNano",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
},
|
|
{
|
|
Name: "httpMethod",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
{
|
|
Name: "non-existent.key",
|
|
},
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{},
|
|
expectedErr: errors.NewInvalidInputf(errors.CodeInvalidInput, "field not found"),
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
statementBuilder := NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
nil,
|
|
fl,
|
|
false,
|
|
100000,
|
|
)
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
q, err := statementBuilder.Build(tenantCtx(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
|
|
|
if c.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expected.Query, q.Query)
|
|
require.Equal(t, c.expected.Args, q.Args)
|
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
requestType qbtypes.RequestType
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
keysMap map[string][]*telemetrytypes.TelemetryFieldKey
|
|
expected qbtypes.Statement
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "List query with empty select fields",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
keysMap: map[string][]*telemetrytypes.TelemetryFieldKey{
|
|
"timestamp": {
|
|
{
|
|
Name: "timestamp",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
"service.name": {
|
|
{
|
|
Name: "service.name",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: mockEvolutionData(time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)),
|
|
},
|
|
},
|
|
},
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, attributes['trace_state'] AS `trace_state`, parent AS `parent_span_id`, toUInt32OrZero(attributes['flags']) AS `flags`, name AS `name`, toInt8(multiIf(kind = 'internal', 1, kind = 'server', 2, kind = 'client', 3, kind = 'producer', 4, kind = 'consumer', 5, 0)) AS `kind`, kind AS `kind_string`, duration AS `duration_nano`, toInt16(multiIf(status = 'error', 2, status = 'ok', 1, 0)) AS `status_code`, attributes['status.message'] AS `status_message`, status AS `status_code_string`, JSONExtractArrayRaw(attributes['span.events']) AS `events`, attributes['links'] AS `links`, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) AS `response_status_code`, url AS `external_http_url`, url AS `http_url`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `external_http_method`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `http_method`, host AS `http_host`, attributes['db.name'] AS `db_name`, attributes['db.operation'] AS `db_operation`, toBool(status = 'error') AS `has_error`, attributes['is_remote'] AS `is_remote`, attributes AS attributes_string, CAST(map() AS Map(String, Float64)) AS attributes_number, CAST(map() AS Map(String, Bool)) AS attributes_bool, map('service.name', toString(service), 'host', toString(host)) AS resources_string FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? LIMIT ?",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with empty select fields and order by timestamp",
|
|
requestType: qbtypes.RequestTypeRaw,
|
|
keysMap: map[string][]*telemetrytypes.TelemetryFieldKey{
|
|
"timestamp": {
|
|
{
|
|
Name: "timestamp",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
"service.name": {
|
|
{
|
|
Name: "service.name",
|
|
Signal: telemetrytypes.SignalTraces,
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: mockEvolutionData(time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)),
|
|
},
|
|
},
|
|
},
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Filter: &qbtypes.Filter{},
|
|
Limit: 10,
|
|
Order: []qbtypes.OrderBy{{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionAsc,
|
|
}},
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "SELECT time AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, attributes['trace_state'] AS `trace_state`, parent AS `parent_span_id`, toUInt32OrZero(attributes['flags']) AS `flags`, name AS `name`, toInt8(multiIf(kind = 'internal', 1, kind = 'server', 2, kind = 'client', 3, kind = 'producer', 4, kind = 'consumer', 5, 0)) AS `kind`, kind AS `kind_string`, duration AS `duration_nano`, toInt16(multiIf(status = 'error', 2, status = 'ok', 1, 0)) AS `status_code`, attributes['status.message'] AS `status_message`, status AS `status_code_string`, JSONExtractArrayRaw(attributes['span.events']) AS `events`, attributes['links'] AS `links`, if(attributes['http.response.status_code'] != '', attributes['http.response.status_code'], attributes['http.status_code']) AS `response_status_code`, url AS `external_http_url`, url AS `http_url`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `external_http_method`, if(attributes['http.request.method'] != '', attributes['http.request.method'], attributes['http.method']) AS `http_method`, host AS `http_host`, attributes['db.name'] AS `db_name`, attributes['db.operation'] AS `db_operation`, toBool(status = 'error') AS `has_error`, attributes['is_remote'] AS `is_remote`, attributes AS attributes_string, CAST(map() AS Map(String, Float64)) AS attributes_number, CAST(map() AS Map(String, Bool)) AS attributes_bool, map('service.name', toString(service), 'host', toString(host)) AS resources_string FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY time AS `timestamp` asc LIMIT ?",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = c.keysMap
|
|
if mockMetadataStore.KeysMap == nil {
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
}
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
statementBuilder := NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
nil,
|
|
fl,
|
|
false,
|
|
100000,
|
|
)
|
|
|
|
q, err := statementBuilder.Build(tenantCtx(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
|
|
|
if c.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expected.Query, q.Query)
|
|
require.Equal(t, c.expected.Args, q.Args)
|
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatementBuilderGroupByResourceEvolution(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
|
|
cases := []struct {
|
|
name string
|
|
startMs uint64
|
|
endMs uint64
|
|
expected qbtypes.Statement
|
|
}{
|
|
{
|
|
name: "window straddles release - both JSON and map branches",
|
|
startMs: 1747947419000, // 2025-05-22 21:56:59 UTC, ~3m before release
|
|
endMs: 1747983448000, // 2025-05-23 07:57:28 UTC, ~10h after release
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant},
|
|
},
|
|
},
|
|
{
|
|
name: "window after release - JSON column only",
|
|
startMs: 1747960000000, // 2025-05-23 00:26:40 UTC, ~2.5h after release
|
|
endMs: 1747983448000, // 2025-05-23 07:57:28 UTC
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747960000000000000", "1747983448000000000", uint64(1747958200), uint64(1747983448), testTenant, 10, "1747960000000000000", "1747983448000000000", uint64(1747958200), uint64(1747983448), testTenant},
|
|
},
|
|
},
|
|
{
|
|
name: "window before release - map column only",
|
|
startMs: 1747900000000, // 2025-05-22 08:26:40 UTC, ~13.5h before release
|
|
endMs: 1747947000000, // 2025-05-22 21:50:00 UTC, ~10m before release
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __limit_cte AS (SELECT toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? GROUP BY `service.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(time, INTERVAL 30 SECOND) AS ts, toString(multiIf(notEmpty(service), service, NULL)) AS `service.name`, count() AS __result_0 FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? AND (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747900000000000000", "1747947000000000000", uint64(1747898200), uint64(1747947000), testTenant, 10, "1747900000000000000", "1747947000000000000", uint64(1747898200), uint64(1747947000), testTenant},
|
|
},
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
statementBuilder := NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
nil,
|
|
fl,
|
|
false,
|
|
100000,
|
|
)
|
|
|
|
query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{Expression: "count()"},
|
|
},
|
|
Filter: &qbtypes.Filter{},
|
|
Limit: 10,
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
q, err := statementBuilder.Build(tenantCtx(), c.startMs, c.endMs, qbtypes.RequestTypeTimeSeries, query, nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expected.Query, q.Query)
|
|
require.Equal(t, c.expected.Args, q.Args)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStatementBuilderTraceQuery(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
requestType qbtypes.RequestType
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
expected qbtypes.Statement
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "List query with non-mat selected fields",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __resource_filter AS (SELECT fingerprint FROM event.span_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), __toe AS (SELECT trace_id FROM event.span WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with mat selected fields",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "materialized.key.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE (attributes['materialized.key.name'] = ? AND mapContains(attributes, 'materialized.key.name') = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"redis-manual", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query with mat selected fields using or and regexp",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "materialized.key.name REGEXP 'redis-manual' OR service.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE ((match(attributes['materialized.key.name'], ?) AND mapContains(attributes, 'materialized.key.name') = ?) OR service = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "List query without any filter",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "list query with span scope filter",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "isentrypoint = true",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE ((name, service) GLOBAL IN (SELECT DISTINCT name, serviceName from event.operation WHERE time >= toDateTime(1747947419))) AND parent != '' AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "list query with span scope filter Or mat field",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "isentrypoint = true or materialized.key.name = 'redis-manual'",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE (((name, service) GLOBAL IN (SELECT DISTINCT name, serviceName from event.operation WHERE time >= toDateTime(1747947419))) AND parent != '' OR (attributes['materialized.key.name'] = ? AND mapContains(attributes, 'materialized.key.name') = ?)) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{"redis-manual", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "list query with deprecated filter field",
|
|
requestType: qbtypes.RequestTypeTrace,
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "kind = 2 or spanKind = 'Server'",
|
|
},
|
|
Limit: 10,
|
|
},
|
|
expected: qbtypes.Statement{
|
|
Query: "WITH __toe AS (SELECT trace_id FROM event.span WHERE (toFloat64(toInt8(multiIf(kind = 'internal', 1, kind = 'server', 2, kind = 'client', 3, kind = 'producer', 4, kind = 'consumer', 5, 0))) = ? OR kind = ?) AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ?), __toe_duration_sorted AS (SELECT trace_id, duration AS duration_nano, service as `service.name`, name FROM event.span WHERE parent = '' AND trace_id GLOBAL IN __toe AND time >= ? AND time < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND org = ? ORDER BY duration_nano DESC LIMIT 1 BY trace_id) 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` FROM __toe INNER JOIN __toe_duration_sorted ON __toe.trace_id = __toe_duration_sorted.trace_id GROUP BY trace_id, duration_nano, name, `service.name` ORDER BY duration_nano DESC LIMIT 1 BY trace_id LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
|
|
Args: []any{float64(2), "Server", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), testTenant, 10},
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
|
|
|
|
statementBuilder := NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
nil,
|
|
fl,
|
|
false,
|
|
100000,
|
|
)
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
|
|
q, err := statementBuilder.Build(tenantCtx(), 1747947419000, 1747983448000, c.requestType, c.query, nil)
|
|
|
|
if c.expectedErr != nil {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), c.expectedErr.Error())
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, c.expected.Query, q.Query)
|
|
require.Equal(t, c.expected.Args, q.Args)
|
|
require.Equal(t, c.expected.Warnings, q.Warnings)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdjustKey(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
inputKey telemetrytypes.TelemetryFieldKey
|
|
keysMap map[string][]*telemetrytypes.TelemetryFieldKey
|
|
expectedKey telemetrytypes.TelemetryFieldKey
|
|
}{
|
|
{
|
|
name: "intrinsic field with no metadata match - override",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "trace_id",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: IntrinsicFields["trace_id"],
|
|
},
|
|
{
|
|
name: "intrinsic field with metadata match with incorrect context - override",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "duration_nano",
|
|
FieldContext: telemetrytypes.FieldContextBody, // incorrect context
|
|
FieldDataType: telemetrytypes.FieldDataTypeInt64,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "duration_nano",
|
|
FieldContext: telemetrytypes.FieldContextSpan, // should be corrected
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber, // modified
|
|
},
|
|
},
|
|
{
|
|
name: "intrinsic field with metadata match with correct context - no override",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "duration_nano",
|
|
FieldContext: telemetrytypes.FieldContextSpan, // correct context
|
|
FieldDataType: telemetrytypes.FieldDataTypeInt64,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "duration_nano",
|
|
FieldContext: telemetrytypes.FieldContextSpan, // should be corrected
|
|
FieldDataType: telemetrytypes.FieldDataTypeInt64, // not modified
|
|
},
|
|
},
|
|
{
|
|
name: "single matching key in metadata - override",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: *buildCompleteFieldKeyMap(releaseTime)["service.name"][0],
|
|
},
|
|
{
|
|
name: "single matching key with context specified - override",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: *buildCompleteFieldKeyMap(releaseTime)["cart.items_count"][0],
|
|
},
|
|
{
|
|
name: "multiple matching keys - all materialized",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "multi.mat.key",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: map[string][]*telemetrytypes.TelemetryFieldKey{
|
|
"multi.mat.key": {
|
|
{
|
|
Name: "multi.mat.key",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: true,
|
|
},
|
|
{
|
|
Name: "multi.mat.key",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeNumber,
|
|
Materialized: true,
|
|
},
|
|
},
|
|
},
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "multi.mat.key",
|
|
Materialized: true,
|
|
},
|
|
},
|
|
{
|
|
name: "multiple matching keys - mixed materialization",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "mixed.materialization.key",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "mixed.materialization.key",
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
{
|
|
name: "multiple matching keys with context specified",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "mixed.materialization.key",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "mixed.materialization.key",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: true,
|
|
},
|
|
},
|
|
{
|
|
name: "no matching keys - unknown field",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "unknown.field",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "unknown.field",
|
|
Materialized: false,
|
|
},
|
|
},
|
|
{
|
|
name: "no matching keys with context filter",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
{
|
|
name: "materialized field",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
|
Materialized: true,
|
|
},
|
|
},
|
|
{
|
|
name: "non-materialized field",
|
|
inputKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
// Create a copy of the input key to avoid modifying the original
|
|
key := c.inputKey
|
|
|
|
// Call adjustKey
|
|
adjustTraceKey(&key, c.keysMap)
|
|
|
|
// Verify the key was adjusted as expected
|
|
require.Equal(t, c.expectedKey.Name, key.Name, "key name should match")
|
|
require.Equal(t, c.expectedKey.FieldContext, key.FieldContext, "field context should match")
|
|
require.Equal(t, c.expectedKey.FieldDataType, key.FieldDataType, "field data type should match")
|
|
require.Equal(t, c.expectedKey.Materialized, key.Materialized, "materialized should match")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdjustKeys(t *testing.T) {
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]
|
|
keysMap map[string][]*telemetrytypes.TelemetryFieldKey
|
|
expectedSelectFields []telemetrytypes.TelemetryFieldKey
|
|
expectedGroupBy []qbtypes.GroupByKey
|
|
expectedOrder []qbtypes.OrderBy
|
|
expectDeprecatedFieldsAdd bool
|
|
}{
|
|
{
|
|
name: "adjust select fields",
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedSelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
|
Materialized: true,
|
|
},
|
|
},
|
|
expectDeprecatedFieldsAdd: true,
|
|
},
|
|
{
|
|
name: "adjust group by fields",
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedGroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
},
|
|
expectDeprecatedFieldsAdd: true,
|
|
},
|
|
{
|
|
name: "adjust order by fields",
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionAsc,
|
|
},
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedOrder: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionAsc,
|
|
},
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "cart.items_count",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeFloat64,
|
|
Materialized: true,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
expectDeprecatedFieldsAdd: true,
|
|
},
|
|
{
|
|
name: "adjust all field types together",
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "trace_id",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
GroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
},
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
FieldDataType: telemetrytypes.FieldDataTypeUnspecified,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
expectedSelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "trace_id",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
expectedGroupBy: []qbtypes.GroupByKey{
|
|
{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
},
|
|
expectedOrder: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Materialized: false,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
expectDeprecatedFieldsAdd: true,
|
|
},
|
|
{
|
|
name: "adjust keys for alias expressions in aggregations - order by",
|
|
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Aggregations: []qbtypes.TraceAggregation{
|
|
{
|
|
Expression: "sum(span.duration)",
|
|
Alias: "span.duration",
|
|
},
|
|
},
|
|
Order: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "duration",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
},
|
|
keysMap: buildCompleteFieldKeyMap(releaseTime),
|
|
// After alias adjustment, name becomes "span.duration" with FieldContextUnspecified
|
|
// "span.duration" is not in keysMap, so context stays unspecified
|
|
expectedOrder: []qbtypes.OrderBy{
|
|
{
|
|
Key: qbtypes.OrderByKey{
|
|
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
|
Name: "span.duration",
|
|
FieldContext: telemetrytypes.FieldContextUnspecified,
|
|
},
|
|
},
|
|
Direction: qbtypes.OrderDirectionDesc,
|
|
},
|
|
},
|
|
expectDeprecatedFieldsAdd: true,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
// Create a deep copy of the keys map to avoid modifying the original
|
|
keysMapCopy := make(map[string][]*telemetrytypes.TelemetryFieldKey)
|
|
for k, v := range c.keysMap {
|
|
keysMapCopy[k] = make([]*telemetrytypes.TelemetryFieldKey, len(v))
|
|
copy(keysMapCopy[k], v)
|
|
}
|
|
|
|
// Call adjustKeys
|
|
adjustTraceKeys(keysMapCopy, &c.query, qbtypes.RequestTypeScalar)
|
|
|
|
// Verify select fields were adjusted
|
|
if c.expectedSelectFields != nil {
|
|
require.Len(t, c.query.SelectFields, len(c.expectedSelectFields))
|
|
for i, expected := range c.expectedSelectFields {
|
|
require.Equal(t, expected.Name, c.query.SelectFields[i].Name, "select field %d name should match", i)
|
|
require.Equal(t, expected.FieldContext, c.query.SelectFields[i].FieldContext, "select field %d context should match", i)
|
|
require.Equal(t, expected.FieldDataType, c.query.SelectFields[i].FieldDataType, "select field %d data type should match", i)
|
|
require.Equal(t, expected.Materialized, c.query.SelectFields[i].Materialized, "select field %d materialized should match", i)
|
|
}
|
|
}
|
|
|
|
// Verify group by fields were adjusted
|
|
if c.expectedGroupBy != nil {
|
|
require.Len(t, c.query.GroupBy, len(c.expectedGroupBy))
|
|
for i, expected := range c.expectedGroupBy {
|
|
require.Equal(t, expected.Name, c.query.GroupBy[i].Name, "group by field %d name should match", i)
|
|
require.Equal(t, expected.FieldContext, c.query.GroupBy[i].FieldContext, "group by field %d context should match", i)
|
|
require.Equal(t, expected.FieldDataType, c.query.GroupBy[i].FieldDataType, "group by field %d data type should match", i)
|
|
require.Equal(t, expected.Materialized, c.query.GroupBy[i].Materialized, "group by field %d materialized should match", i)
|
|
}
|
|
}
|
|
|
|
// Verify order by fields were adjusted
|
|
if c.expectedOrder != nil {
|
|
require.Len(t, c.query.Order, len(c.expectedOrder))
|
|
for i, expected := range c.expectedOrder {
|
|
require.Equal(t, expected.Key.Name, c.query.Order[i].Key.Name, "order field %d name should match", i)
|
|
require.Equal(t, expected.Key.FieldContext, c.query.Order[i].Key.FieldContext, "order field %d context should match", i)
|
|
require.Equal(t, expected.Key.FieldDataType, c.query.Order[i].Key.FieldDataType, "order field %d data type should match", i)
|
|
require.Equal(t, expected.Key.Materialized, c.query.Order[i].Key.Materialized, "order field %d materialized should match", i)
|
|
require.Equal(t, expected.Direction, c.query.Order[i].Direction, "order field %d direction should match", i)
|
|
}
|
|
}
|
|
|
|
// Verify deprecated fields were added to the keys map
|
|
if c.expectDeprecatedFieldsAdd {
|
|
// Check that at least some deprecated fields were added
|
|
foundDeprecatedField := false
|
|
for fieldName := range IntrinsicFieldsDeprecated {
|
|
if _, ok := keysMapCopy[fieldName]; ok {
|
|
foundDeprecatedField = true
|
|
break
|
|
}
|
|
}
|
|
if !foundDeprecatedField {
|
|
for fieldName := range CalculatedFieldsDeprecated {
|
|
if _, ok := keysMapCopy[fieldName]; ok {
|
|
foundDeprecatedField = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
require.True(t, foundDeprecatedField, "at least one deprecated field should be added to keys map")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSkipResourceFingerprint exercises the three resolver outcomes when
|
|
// skip_resource_fingerprint is enabled: use-CTE (count < threshold),
|
|
// fallback (count >= threshold), and the legacy path (feature disabled).
|
|
func TestSkipResourceFingerprint(t *testing.T) {
|
|
const (
|
|
startMs = uint64(1747947419000)
|
|
endMs = uint64(1747983448000)
|
|
threshold = uint64(10)
|
|
)
|
|
|
|
query := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
|
Signal: telemetrytypes.SignalTraces,
|
|
Filter: &qbtypes.Filter{
|
|
Expression: "service.name = 'redis-manual'",
|
|
},
|
|
SelectFields: []telemetrytypes.TelemetryFieldKey{
|
|
{
|
|
Name: "name",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
},
|
|
Limit: 5,
|
|
}
|
|
|
|
t.Run("disabled uses the legacy CTE", func(t *testing.T) {
|
|
sb := newSkipResourceFingerprintBuilder(t, nil, false, threshold)
|
|
|
|
stmt, err := sb.Build(tenantCtx(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
|
require.NoError(t, err)
|
|
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
|
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
|
})
|
|
|
|
t.Run("CTE attached when count below threshold", func(t *testing.T) {
|
|
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
|
mock := mockStore.Mock()
|
|
|
|
// Only the count query runs against the telemetry store; the CTE
|
|
// itself is embedded as SQL in the main query (no extra round trip).
|
|
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM event\.span_resource`).
|
|
WillReturnRow(dsmock.NewRow([]dsmock.ColumnType{
|
|
{Name: "count", Type: "UInt64"},
|
|
}, []any{uint64(2)}))
|
|
|
|
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
|
|
|
|
stmt, err := sb.Build(tenantCtx(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.Contains(t, stmt.Query, "__resource_filter AS (SELECT fingerprint")
|
|
require.Contains(t, stmt.Query, "resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
|
|
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("fallback when count at or above threshold", func(t *testing.T) {
|
|
mockStore := telemetrystoretest.New(telemetrystore.Config{}, ®exQueryMatcher{})
|
|
mock := mockStore.Mock()
|
|
|
|
mock.ExpectQueryRow(`SELECT count\(\) FROM \(SELECT fingerprint FROM event\.span_resource`).
|
|
WillReturnRow(dsmock.NewRow([]dsmock.ColumnType{
|
|
{Name: "count", Type: "UInt64"},
|
|
}, []any{threshold}))
|
|
|
|
sb := newSkipResourceFingerprintBuilder(t, mockStore, true, threshold)
|
|
|
|
stmt, err := sb.Build(tenantCtx(), startMs, endMs, qbtypes.RequestTypeRaw, query, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.NotContains(t, stmt.Query, "__resource_filter AS")
|
|
require.NotContains(t, stmt.Query, "resource_fingerprint")
|
|
// resource conditions are pushed onto the main table via the promoted
|
|
// service column, which is where the envelope keeps service.name
|
|
require.Contains(t, stmt.Query, "service = ?")
|
|
|
|
require.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func newSkipResourceFingerprintBuilder(
|
|
t *testing.T,
|
|
telemetryStore telemetrystore.TelemetryStore,
|
|
skipEnable bool,
|
|
threshold uint64,
|
|
) *traceQueryStatementBuilder {
|
|
t.Helper()
|
|
|
|
fm := NewFieldMapper()
|
|
cb := NewConditionBuilder(fm)
|
|
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
|
|
releaseTime := time.Date(2025, 5, 22, 22, 0, 0, 0, time.UTC)
|
|
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
|
|
|
|
fl := flaggertest.New(t)
|
|
aggExprRewriter := querybuilder.NewAggExprRewriter(
|
|
instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl,
|
|
)
|
|
|
|
return NewTraceQueryStatementBuilder(
|
|
instrumentationtest.New().ToProviderSettings(),
|
|
mockMetadataStore,
|
|
fm,
|
|
cb,
|
|
aggExprRewriter,
|
|
telemetryStore,
|
|
fl,
|
|
skipEnable,
|
|
threshold,
|
|
)
|
|
}
|