Files
o11y/pkg/telemetrylogs/condition_builder_test.go
zeekayandHanzo Dev ded468300a readers: the query builders speak the envelope, not the fork's dead dialect
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]>
2026-08-01 13:11:33 -07:00

854 lines
27 KiB
Go

package telemetrylogs
import (
"context"
"testing"
"time"
"github.com/hanzoai/o11y/pkg/datastoresql"
"github.com/hanzo-ds/sqlbuilder"
"github.com/hanzoai/o11y/pkg/flagger/flaggertest"
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExistsConditionForWithEvolutions(t *testing.T) {
testCases := []struct {
name string
startTs uint64
endTs uint64
key telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
expectedSQL string
expectedArgs []any
expectedError error
}{
{
name: "New column",
startTs: uint64(time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
endTs: uint64(time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Evolutions: []*telemetrytypes.EvolutionEntry{
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resources_string",
FieldContext: telemetrytypes.FieldContextResource,
ColumnType: "Map(LowCardinality(String), String)",
FieldName: "__all__",
ReleaseTime: time.Unix(0, 0),
},
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resource",
ColumnType: "JSON()",
FieldContext: telemetrytypes.FieldContextResource,
FieldName: "__all__",
ReleaseTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE notEmpty(service)",
expectedError: nil,
},
{
name: "Old column",
startTs: uint64(time.Date(2023, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
endTs: uint64(time.Date(2023, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Evolutions: []*telemetrytypes.EvolutionEntry{
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resources_string",
FieldContext: telemetrytypes.FieldContextResource,
ColumnType: "Map(LowCardinality(String), String)",
FieldName: "__all__",
ReleaseTime: time.Unix(0, 0),
},
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resource",
ColumnType: "JSON()",
FieldContext: telemetrytypes.FieldContextResource,
FieldName: "__all__",
ReleaseTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE notEmpty(service)",
expectedArgs: nil,
expectedError: nil,
},
{
name: "Both Old column and new - empty filter",
startTs: uint64(time.Date(2023, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
endTs: uint64(time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC).UnixNano()),
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Evolutions: []*telemetrytypes.EvolutionEntry{
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resources_string",
FieldContext: telemetrytypes.FieldContextResource,
ColumnType: "Map(LowCardinality(String), String)",
FieldName: "__all__",
ReleaseTime: time.Unix(0, 0),
},
{
Signal: telemetrytypes.SignalLogs,
ColumnName: "resource",
ColumnType: "JSON()",
FieldContext: telemetrytypes.FieldContextResource,
FieldName: "__all__",
ReleaseTime: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE notEmpty(service)",
expectedError: nil,
},
}
fl := flaggertest.New(t)
fm := NewFieldMapper(fl)
conditionBuilder := NewConditionBuilder(fm, fl)
ctx := context.Background()
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
cond, err := conditionBuilder.ConditionFor(ctx, tc.startTs, tc.endTs, &tc.key, tc.operator, tc.value, sb)
sb.Where(cond)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, args := sb.BuildWithFlavor(datastoresql.Flavor)
assert.Contains(t, sql, tc.expectedSQL)
assert.Equal(t, tc.expectedArgs, args)
}
})
}
}
func TestConditionFor(t *testing.T) {
ctx := context.Background()
mockEvolution := mockEvolutionData(time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC))
testCases := []struct {
name string
key telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
evolutions []*telemetrytypes.EvolutionEntry
expectedSQL string
expectedArgs []any
expectedError error
}{
{
name: "Equal operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorEqual,
value: "error message",
expectedSQL: "body = ?",
expectedArgs: []any{"error message"},
expectedError: nil,
},
{
name: "Greater Than operator - number attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.duration",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
operator: qbtypes.FilterOperatorGreaterThan,
value: float64(100),
expectedSQL: "(toFloat64(toFloat64OrNull(attributes['request.duration'])) > ? AND mapContains(attributes, 'request.duration') = ?)",
expectedArgs: []any{float64(100), true},
expectedError: nil,
},
{
name: "Less Than operator - number attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "request.size",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeNumber,
},
operator: qbtypes.FilterOperatorLessThan,
value: float64(1024),
expectedSQL: "(toFloat64(toFloat64OrNull(attributes['request.size'])) < ? AND mapContains(attributes, 'request.size') = ?)",
expectedArgs: []any{float64(1024), true},
expectedError: nil,
},
{
name: "Like operator - body",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorLike,
value: "%error%",
expectedSQL: "body ILIKE ?",
expectedArgs: []any{"%error%"},
expectedError: nil,
},
{
name: "Not Like operator - body",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorNotLike,
value: "%error%",
expectedSQL: "body NOT ILIKE ?",
expectedArgs: []any{"%error%"},
expectedError: nil,
},
{
name: "ILike operator - string attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorILike,
value: "%admin%",
expectedSQL: "(attributes['user.id'] ILIKE ? AND mapContains(attributes, 'user.id') = ?)",
expectedArgs: []any{"%admin%", true},
expectedError: nil,
},
{
name: "Not ILike operator - string attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotILike,
value: "%admin%",
expectedSQL: "WHERE attributes['user.id'] NOT ILIKE ?",
expectedArgs: []any{"%admin%"},
expectedError: nil,
},
{
name: "Contains operator - string attribute number value",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorContains,
value: 521509198310,
expectedSQL: "attributes['user.id'] ILIKE ?",
expectedArgs: []any{"%521509198310%", true},
expectedError: nil,
},
{
name: "Contains operator - body",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorContains,
value: 521509198310,
expectedSQL: "body ILIKE ?",
expectedArgs: []any{"%521509198310%"},
expectedError: nil,
},
{
name: "Contains operator - string attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorContains,
value: "admin",
expectedSQL: "(attributes['user.id'] ILIKE ? AND mapContains(attributes, 'user.id') = ?)",
expectedArgs: []any{"%admin%", true},
expectedError: nil,
},
{
name: "In operator - severity_text",
key: telemetrytypes.TelemetryFieldKey{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorIn,
value: []any{"error", "fatal", "critical"},
expectedSQL: "(severity_text = ? OR severity_text = ? OR severity_text = ?)",
expectedArgs: []any{"error", "fatal", "critical"},
expectedError: nil,
},
{
name: "In operator - invalid value",
key: telemetrytypes.TelemetryFieldKey{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorIn,
value: "error",
expectedSQL: "",
expectedError: qbtypes.ErrInValues,
},
{
name: "Not In operator - severity_text",
key: telemetrytypes.TelemetryFieldKey{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorNotIn,
value: []any{"debug", "info", "trace"},
expectedSQL: "(severity_text <> ? AND severity_text <> ? AND severity_text <> ?)",
expectedArgs: []any{"debug", "info", "trace"},
expectedError: nil,
},
{
name: "Exists operator - string field",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "WHERE body <> ?",
expectedArgs: []any{""},
expectedError: nil,
},
{
name: "Not Exists operator - string field",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "WHERE body = ?",
expectedArgs: []any{""},
expectedError: nil,
},
{
name: "Exists operator - map field",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "mapContains(attributes, 'user.id') = ?",
expectedArgs: []any{true},
expectedError: nil,
},
{
name: "Not Exists operator - map field",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.id",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "mapContains(attributes, 'user.id') <> ?",
expectedArgs: []any{true},
expectedError: nil,
},
{
name: "Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
evolutions: mockEvolution,
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: "notEmpty(service)",
expectedArgs: nil,
expectedError: nil,
},
{
name: "Not Exists operator - json field",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
evolutions: mockEvolution,
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: "empty(service)",
expectedArgs: nil,
expectedError: nil,
},
{
name: "Regexp operator - body field",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorRegexp,
value: "error.*failed",
expectedSQL: "match(LOWER(body), LOWER(?))",
expectedArgs: []any{"error.*failed"},
expectedError: nil,
},
{
name: "Not Regexp operator - body field",
key: telemetrytypes.TelemetryFieldKey{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "debug|trace",
expectedSQL: "NOT match(LOWER(body), LOWER(?))",
expectedArgs: []any{"debug|trace"},
expectedError: nil,
},
{
name: "Regexp operator - string attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.url",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorRegexp,
value: "^https://.*\\.example\\.com.*$",
expectedSQL: "(match(attributes['http.url'], ?) AND mapContains(attributes, 'http.url') = ?)",
expectedArgs: []any{"^https://.*\\.example\\.com.*$", true},
expectedError: nil,
},
{
name: "Not Regexp operator - string attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.url",
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "^http://localhost.*",
expectedSQL: "WHERE NOT match(attributes['http.url'], ?)",
expectedArgs: []any{"^http://localhost.*"},
expectedError: nil,
},
{
name: "Regexp operator - resource attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
},
evolutions: mockEvolution,
operator: qbtypes.FilterOperatorRegexp,
value: "frontend-.*",
expectedSQL: "WHERE match(service, ?)",
expectedArgs: []any{"frontend-.*"},
expectedError: nil,
},
{
name: "Not Regexp operator - resource attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "service.name",
FieldContext: telemetrytypes.FieldContextResource,
FieldDataType: telemetrytypes.FieldDataTypeString,
Materialized: true,
},
evolutions: mockEvolution,
operator: qbtypes.FilterOperatorNotRegexp,
value: "test-.*",
expectedSQL: "WHERE NOT match(service, ?)",
expectedArgs: []any{"test-.*"},
expectedError: nil,
},
{
name: "Regexp operator - severity_text",
key: telemetrytypes.TelemetryFieldKey{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorRegexp,
value: "ERROR|FATAL|CRITICAL",
expectedSQL: "match(severity_text, ?)",
expectedArgs: []any{"ERROR|FATAL|CRITICAL"},
expectedError: nil,
},
{
name: "Not Regexp operator - severity_text",
key: telemetrytypes.TelemetryFieldKey{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "DEBUG|TRACE",
expectedSQL: "NOT match(severity_text, ?)",
expectedArgs: []any{"DEBUG|TRACE"},
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{
Name: "nonexistent_field",
FieldContext: telemetrytypes.FieldContextLog,
},
operator: qbtypes.FilterOperatorEqual,
value: "value",
expectedSQL: "",
expectedError: qbtypes.ErrColumnNotFound,
},
}
fl := flaggertest.New(t)
fm := NewFieldMapper(fl)
conditionBuilder := NewConditionBuilder(fm, fl)
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
tc.key.Evolutions = tc.evolutions
cond, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, tc.operator, tc.value, sb)
sb.Where(cond)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, args := sb.BuildWithFlavor(datastoresql.Flavor)
assert.Contains(t, sql, tc.expectedSQL)
assert.Equal(t, tc.expectedArgs, args)
}
})
}
}
func TestConditionForMultipleKeys(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
keys []telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
expectedSQL string
expectedArgs []any
expectedError error
}{
{
name: "Equal operator - string",
keys: []telemetrytypes.TelemetryFieldKey{
{
Name: "body",
FieldContext: telemetrytypes.FieldContextLog,
},
{
Name: "severity_text",
FieldContext: telemetrytypes.FieldContextLog,
},
},
operator: qbtypes.FilterOperatorEqual,
value: "error message",
expectedSQL: "body = ? AND severity_text = ?",
expectedArgs: []any{"error message", "error message"},
expectedError: nil,
},
}
fl := flaggertest.New(t)
fm := NewFieldMapper(fl)
conditionBuilder := NewConditionBuilder(fm, fl)
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
var err error
for _, key := range tc.keys {
cond, err := conditionBuilder.conditionFor(ctx, 0, 0, &key, tc.operator, tc.value, sb)
sb.Where(cond)
if err != nil {
t.Fatalf("Error getting condition for key %s: %v", key.Name, err)
}
}
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, _ := sb.BuildWithFlavor(datastoresql.Flavor)
assert.Contains(t, sql, tc.expectedSQL)
}
})
}
}
func TestConditionForJSONBodySearch(t *testing.T) {
ctx := context.Background()
testCases := []struct {
name string
key telemetrytypes.TelemetryFieldKey
operator qbtypes.FilterOperator
value any
expectedSQL string
expectedError error
}{
{
name: "Equal operator - int64",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorEqual,
value: 200,
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ?`,
expectedError: nil,
},
{
name: "Equal operator - float64",
key: telemetrytypes.TelemetryFieldKey{
Name: "duration_ms",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorEqual,
value: 405.5,
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."duration_ms"'), 'Float64') = ?`,
expectedError: nil,
},
{
name: "Equal operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.method",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorEqual,
value: "GET",
expectedSQL: `JSON_VALUE(body, '$."http"."method"') = ?`,
expectedError: nil,
},
{
name: "Equal operator - bool",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.success",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorEqual,
value: true,
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."success"'), 'Bool') = ?`,
expectedError: nil,
},
{
name: "Exists operator",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorExists,
value: nil,
expectedSQL: `JSON_EXISTS(body, '$."http"."status_code"')`,
expectedError: nil,
},
{
name: "Not Exists operator",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorNotExists,
value: nil,
expectedSQL: `NOT JSON_EXISTS(body, '$."http"."status_code"')`,
expectedError: nil,
},
{
name: "Greater than operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorGreaterThan,
value: "200",
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') > ?`,
expectedError: nil,
},
{
name: "Greater than operator - int64",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorGreaterThan,
value: 200,
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') > ?`,
expectedError: nil,
},
{
name: "Less than operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorLessThan,
value: "300",
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') < ?`,
expectedError: nil,
},
{
name: "Less than operator - int64",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorLessThan,
value: 300,
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') < ?`,
expectedError: nil,
},
{
name: "Contains operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorContains,
value: "200",
expectedSQL: `JSON_VALUE(body, '$."http"."status_code"') ILIKE ?`,
expectedError: nil,
},
{
name: "Not Contains operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorNotContains,
value: "200",
expectedSQL: `JSON_VALUE(body, '$."http"."status_code"') NOT ILIKE ?`,
expectedError: nil,
},
{
name: "Between operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorBetween,
value: []any{"200", "300"},
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') BETWEEN ? AND ?`,
expectedError: nil,
},
{
name: "Between operator - int64",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorBetween,
value: []any{400, 500},
expectedSQL: `JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') BETWEEN ? AND ?`,
expectedError: nil,
},
{
name: "In operator - string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorIn,
value: []any{"200", "300"},
expectedSQL: `(JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ? OR JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ?)`,
expectedError: nil,
},
{
name: "In operator - int64",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.status_code",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorIn,
value: []any{401, 404, 500},
expectedSQL: `(JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ? OR JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ? OR JSONExtract(JSON_VALUE(body, '$."http"."status_code"'), 'Int64') = ?)`,
expectedError: nil,
},
{
name: "Regexp operator - json body string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.method",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorRegexp,
value: "GET|POST|PUT",
expectedSQL: `match(JSON_VALUE(body, '$."http"."method"'), ?)`,
expectedError: nil,
},
{
name: "Not Regexp operator - json body string",
key: telemetrytypes.TelemetryFieldKey{
Name: "http.method",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "DELETE|PATCH",
expectedSQL: `NOT match(JSON_VALUE(body, '$."http"."method"'), ?)`,
expectedError: nil,
},
{
name: "Regexp operator - json body with dots in path",
key: telemetrytypes.TelemetryFieldKey{
Name: "user.email",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorRegexp,
value: "^.*@example\\.com$",
expectedSQL: `match(JSON_VALUE(body, '$."user"."email"'), ?)`,
expectedError: nil,
},
{
name: "Not Regexp operator - json body nested path",
key: telemetrytypes.TelemetryFieldKey{
Name: "response.headers.content-type",
FieldContext: telemetrytypes.FieldContextBody,
},
operator: qbtypes.FilterOperatorNotRegexp,
value: "^text/.*",
expectedSQL: `NOT match(JSON_VALUE(body, '$."response"."headers"."content-type"'), ?)`,
expectedError: nil,
},
}
fl := flaggertest.New(t)
fm := NewFieldMapper(fl)
conditionBuilder := NewConditionBuilder(fm, fl)
for _, tc := range testCases {
sb := sqlbuilder.NewSelectBuilder()
t.Run(tc.name, func(t *testing.T) {
cond, err := conditionBuilder.conditionFor(ctx, 0, 0, &tc.key, tc.operator, tc.value, sb)
sb.Where(cond)
if tc.expectedError != nil {
assert.Equal(t, tc.expectedError, err)
} else {
require.NoError(t, err)
sql, _ := sb.BuildWithFlavor(datastoresql.Flavor)
assert.Contains(t, sql, tc.expectedSQL)
}
})
}
}