mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
The traces and logs statement/condition builders emitted the OTLP-fork columns
(timestamp, duration_nano, attributes_string/number/bool, resources_string,
resource_string_service$name, ts_bucket_start, parent_span_id, trace_flags,
scope_name) against event.span / event.log, which carry the 15-column envelope.
Every span lookup and log search failed Code 47 UNKNOWN_IDENTIFIER — executed and
proven before this change, not inferred.
TWO COLUMNS ADDED to event.span and event.log because no rewrite can satisfy them:
resource_fingerprint String — the builders stamp
'resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)'
on the MAIN table, and __resource_filter reads event.span_resource /
event.log_resource keyed (org, seen_at_ts_bucket_start, fingerprint). The
fingerprint is a write-time hash of the whole resource label set — derivable
from NO envelope column. event.log's pre-existing UInt64 'resource' is
type-incompatible and empty.
ts_bucket_start UInt64 MATERIALIZED intDiv(toUnixTimestamp(time),1800)*1800 —
the builders put bucket-range predicates directly on the main table.
MATERIALIZED-from-time keeps writers 15-column-envelope-only and shares ONE
bucketing scheme with the four support tables that already speak
seen_at_ts_bucket_start.
EVERYTHING ELSE REWRITTEN onto the envelope — one naming scheme wins:
timestamp->time, parent_span_id->parent, duration_nano->duration,
kind_string->kind (the live kind IS the string enum, so numeric kind/status_code
predicates translate by enum NAME), status_code_string->status,
has_error->status='error', attributes_string[k]->attributes[k],
attributes_number[k]->toFloat64OrNull(attributes[k]),
attributes_bool[k]->attributes[k]='true', resources_string['service.name'] and
resource_string_service$name->service, http_host->host, the calculated columns
to attribute expressions, and the 28 deprecated camelCase aliases through
oldToNew to the SAME envelope targets — never to table columns.
spantypes TraceDB/TraceSummaryTable move o11y_traces/distributed_trace_summary ->
event/trace; impltracedetail builds its FROM from those constants. Swapping them
ALONE panics TestGetTraceSummary on a nil pointer — the store and its test move
together, which is why they are in this commit and not a smaller one.
Pins moved deliberately, each with the reason in place. live_proof_test.go in
three packages executes the builders' emitted SQL against the real store, so the
next dialect drift fails loudly instead of at runtime.
go build ./... and go test ./... green across every package.
Co-authored-by: Hanzo Dev <[email protected]>
1703 lines
87 KiB
Go
1703 lines
87 KiB
Go
package telemetrytraces
|
|
|
|
import (
|
|
"context"
|
|
"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 <= ? 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 (`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), 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "redis-manual", "GET", true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "redis-manual", true, "redis-manual", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "", "", "redis-manual", "", "Server", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, true, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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 <= ? 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 (`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), 10, "", 0, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
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(context.Background(), 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 <= ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ? 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), 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 <= ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ? LIMIT ?",
|
|
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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(context.Background(), 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 <= ? LIMIT ?",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ? ORDER BY time AS `timestamp` asc LIMIT ?",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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(context.Background(), 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 <= ? 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 (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
|
|
},
|
|
},
|
|
{
|
|
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 <= ? 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 (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747960000000000000", "1747983448000000000", uint64(1747958200), uint64(1747983448), 10, "1747960000000000000", "1747983448000000000", uint64(1747958200), uint64(1747983448)},
|
|
},
|
|
},
|
|
{
|
|
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 <= ? 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 (`service.name`) GLOBAL IN (SELECT `service.name` FROM __limit_cte) GROUP BY ts, `service.name`",
|
|
Args: []any{"1747900000000000000", "1747947000000000000", uint64(1747898200), uint64(1747947000), 10, "1747900000000000000", "1747947000000000000", uint64(1747898200), uint64(1747947000)},
|
|
},
|
|
},
|
|
}
|
|
|
|
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(context.Background(), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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 <= ?), __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 <= ? 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), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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,
|
|
)
|
|
}
|