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]>
393 lines
13 KiB
Go
393 lines
13 KiB
Go
package telemetrytraces
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/o11y/pkg/datastoresql"
|
|
|
|
"github.com/hanzo-ds/sqlbuilder"
|
|
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 TestConditionFor(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
mockEvolution := mockEvolutionData(time.Date(2025, 10, 26, 0, 10, 0, 0, time.UTC))
|
|
testCases := []struct {
|
|
name string
|
|
key telemetrytypes.TelemetryFieldKey
|
|
operator qbtypes.FilterOperator
|
|
value any
|
|
expectedSQL string
|
|
expectedArgs []any
|
|
expectedError error
|
|
}{
|
|
{
|
|
name: "Not Equal operator - timestamp",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorNotEqual,
|
|
value: uint64(1617979338000000000),
|
|
expectedSQL: "time <> ?",
|
|
expectedArgs: []any{uint64(1617979338000000000)},
|
|
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: "Greater Than Or Equal operator - timestamp",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorGreaterThanOrEq,
|
|
value: uint64(1617979338000000000),
|
|
expectedSQL: "time >= ?",
|
|
expectedArgs: []any{uint64(1617979338000000000)},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Less Than Or Equal operator - timestamp",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorLessThanOrEq,
|
|
value: uint64(1617979338000000000),
|
|
expectedSQL: "time <= ?",
|
|
expectedArgs: []any{uint64(1617979338000000000)},
|
|
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%", true},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Contains operator - string attribute",
|
|
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: "LIKE operator - string attribute",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
operator: qbtypes.FilterOperatorLike,
|
|
value: 521509198310,
|
|
expectedSQL: "attributes['user.id'] LIKE ?",
|
|
expectedArgs: []any{"521509198310", true},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Between operator - timestamp",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorBetween,
|
|
value: []any{uint64(1617979338000000000), uint64(1617979348000000000)},
|
|
expectedSQL: "time BETWEEN ? AND ?",
|
|
expectedArgs: []any{uint64(1617979338000000000), uint64(1617979348000000000)},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Between operator - invalid value",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorBetween,
|
|
value: "invalid",
|
|
expectedSQL: "",
|
|
expectedError: qbtypes.ErrBetweenValues,
|
|
},
|
|
{
|
|
name: "Between operator - insufficient values",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorBetween,
|
|
value: []any{uint64(1617979338000000000)},
|
|
expectedSQL: "",
|
|
expectedError: qbtypes.ErrBetweenValues,
|
|
},
|
|
{
|
|
name: "Not Between operator - timestamp",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "timestamp",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorNotBetween,
|
|
value: []any{uint64(1617979338000000000), uint64(1617979348000000000)},
|
|
expectedSQL: "time NOT BETWEEN ? AND ?",
|
|
expectedArgs: []any{uint64(1617979338000000000), uint64(1617979348000000000)},
|
|
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') = ?",
|
|
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') <> ?",
|
|
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: "WHERE notEmpty(service)",
|
|
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: "WHERE empty(service)",
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Contains operator - map field",
|
|
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 - map field",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
operator: qbtypes.FilterOperatorIn,
|
|
value: []any{"admin", "user"},
|
|
expectedSQL: "((attributes['user.id'] = ? OR attributes['user.id'] = ?) AND mapContains(attributes, 'user.id') = ?)",
|
|
expectedArgs: []any{"admin", "user", true},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Not In operator - map field",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "user.id",
|
|
FieldContext: telemetrytypes.FieldContextAttribute,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
},
|
|
operator: qbtypes.FilterOperatorNotIn,
|
|
value: []any{"admin", "user"},
|
|
expectedSQL: "(attributes['user.id'] <> ? AND attributes['user.id'] <> ?)",
|
|
expectedArgs: []any{"admin", "user", true},
|
|
expectedError: nil,
|
|
},
|
|
{
|
|
name: "Non-existent column",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "nonexistent_field",
|
|
FieldContext: telemetrytypes.FieldContextSpan,
|
|
},
|
|
operator: qbtypes.FilterOperatorEqual,
|
|
value: "value",
|
|
expectedSQL: "",
|
|
expectedError: qbtypes.ErrColumnNotFound,
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
conditionBuilder := NewConditionBuilder(fm)
|
|
|
|
for _, tc := range testCases {
|
|
sb := sqlbuilder.NewSelectBuilder()
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cond, err := conditionBuilder.ConditionFor(ctx, 1761437108000000000, 1761458708000000000, &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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConditionForResourceWithEvolution(t *testing.T) {
|
|
ctx := context.Background()
|
|
releaseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
evolutions := mockEvolutionData(releaseTime)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
key telemetrytypes.TelemetryFieldKey
|
|
operator qbtypes.FilterOperator
|
|
tsStart uint64
|
|
tsEnd uint64
|
|
expectedSQL string
|
|
}{
|
|
{
|
|
name: "Exists - window after release - JSON only",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: evolutions,
|
|
},
|
|
operator: qbtypes.FilterOperatorExists,
|
|
tsStart: uint64(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
tsEnd: uint64(time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
expectedSQL: "WHERE notEmpty(service)",
|
|
},
|
|
{
|
|
name: "NotExists - window after release - JSON only",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: evolutions,
|
|
},
|
|
operator: qbtypes.FilterOperatorNotExists,
|
|
tsStart: uint64(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
tsEnd: uint64(time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
expectedSQL: "WHERE empty(service)",
|
|
},
|
|
{
|
|
name: "Exists - window before release - map only",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: evolutions,
|
|
},
|
|
operator: qbtypes.FilterOperatorExists,
|
|
tsStart: uint64(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
tsEnd: uint64(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
expectedSQL: "WHERE notEmpty(service)",
|
|
},
|
|
{
|
|
name: "Exists - window straddles release - multiIf null check",
|
|
key: telemetrytypes.TelemetryFieldKey{
|
|
Name: "service.name",
|
|
FieldContext: telemetrytypes.FieldContextResource,
|
|
FieldDataType: telemetrytypes.FieldDataTypeString,
|
|
Evolutions: evolutions,
|
|
},
|
|
operator: qbtypes.FilterOperatorExists,
|
|
tsStart: uint64(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
tsEnd: uint64(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC).UnixNano()),
|
|
expectedSQL: "WHERE notEmpty(service)",
|
|
},
|
|
}
|
|
|
|
fm := NewFieldMapper()
|
|
conditionBuilder := NewConditionBuilder(fm)
|
|
|
|
for _, tc := range testCases {
|
|
sb := sqlbuilder.NewSelectBuilder()
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
cond, err := conditionBuilder.ConditionFor(ctx, tc.tsStart, tc.tsEnd, &tc.key, tc.operator, nil, sb)
|
|
require.NoError(t, err)
|
|
sb.Where(cond)
|
|
sql, _ := sb.BuildWithFlavor(datastoresql.Flavor)
|
|
assert.Contains(t, sql, tc.expectedSQL)
|
|
})
|
|
}
|
|
}
|