Files
o11y/pkg/telemetrylogs/live_proof_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

127 lines
4.6 KiB
Go

package telemetrylogs
// THROWAWAY live-plane proof. Gated on O11Y_LIVE_PROOF=1 + O11Y_PROOF_DSN.
// Builds real statements through the log statement builder and EXECUTES them
// against the live event plane with real arg binding. Deleted after the proof.
import (
"context"
"os"
"testing"
"time"
"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/datastoretelemetrystore"
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"
)
func liveLogTelemetryStore(t *testing.T) telemetrystore.TelemetryStore {
t.Helper()
if os.Getenv("O11Y_LIVE_PROOF") != "1" {
t.Skip("live proof disabled")
}
dsn := os.Getenv("O11Y_PROOF_DSN")
require.NotEmpty(t, dsn, "O11Y_PROOF_DSN required")
ts, err := datastoretelemetrystore.New(
context.Background(),
instrumentationtest.New().ToProviderSettings(),
telemetrystore.Config{
Provider: "datastore",
Connection: telemetrystore.ConnectionConfig{MaxOpenConns: 2, MaxIdleConns: 1, DialTimeout: 5 * time.Second},
Datastore: telemetrystore.DatastoreConfig{DSN: dsn},
},
)
require.NoError(t, err)
return ts
}
func execLogCount(t *testing.T, ts telemetrystore.TelemetryStore, stmt qbtypes.Statement) int {
t.Helper()
rows, err := ts.Datastore().Query(context.Background(), stmt.Query, stmt.Args...)
require.NoError(t, err, "EXEC FAILED\nSQL: %s\nARGS: %v", stmt.Query, stmt.Args)
defer rows.Close()
n := 0
for rows.Next() {
n++
}
require.NoError(t, rows.Err())
return n
}
func TestLiveProofLogs(t *testing.T) {
ts := liveLogTelemetryStore(t)
releaseTime := time.Date(2025, 5, 6, 0, 0, 0, 0, time.UTC)
fl := flaggertest.New(t)
mockMetadataStore := telemetrytypestest.NewMockMetadataStore()
mockMetadataStore.KeysMap = buildCompleteFieldKeyMap(releaseTime)
fm := NewFieldMapper(fl)
cb := NewConditionBuilder(fm, fl)
aggExprRewriter := querybuilder.NewAggExprRewriter(instrumentationtest.New().ToProviderSettings(), nil, fm, cb, nil, fl)
statementBuilder := NewLogQueryStatementBuilder(
instrumentationtest.New().ToProviderSettings(),
mockMetadataStore,
fm,
cb,
aggExprRewriter,
DefaultFullTextColumn,
GetBodyJSONKey,
fl,
nil,
false,
100000,
)
start := uint64(1785540000000)
end := uint64(1785560000000)
t.Run("log search with resource filter CTE and body", func(t *testing.T) {
q := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "service.name = 'o11y-writeside-proof' AND body CONTAINS 'boom' AND severity_text = 'error'"},
Order: []qbtypes.OrderBy{{Key: qbtypes.OrderByKey{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "time"}}, Direction: qbtypes.OrderDirectionDesc}},
Limit: 10,
}
stmt, err := statementBuilder.Build(context.Background(), start, end, qbtypes.RequestTypeRaw, q, nil)
require.NoError(t, err)
n := execLogCount(t, ts, *stmt)
t.Logf("log search rows=%d\nSQL: %s\nARGS: %v", n, stmt.Query, stmt.Args)
require.Greater(t, n, 0, "expected proof log rows")
})
t.Run("log time series by severity", func(t *testing.T) {
q := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
Aggregations: []qbtypes.LogAggregation{{Expression: "count()"}},
Filter: &qbtypes.Filter{Expression: "service.name = 'o11y-writeside-proof'"},
GroupBy: []qbtypes.GroupByKey{{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "severity_text"}}},
Limit: 10,
}
stmt, err := statementBuilder.Build(context.Background(), start, end, qbtypes.RequestTypeTimeSeries, q, nil)
require.NoError(t, err)
n := execLogCount(t, ts, *stmt)
t.Logf("log timeseries rows=%d\nSQL: %s\nARGS: %v", n, stmt.Query, stmt.Args)
require.Greater(t, n, 0)
})
t.Run("log list attribute and trace correlation", func(t *testing.T) {
q := qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]{
Signal: telemetrytypes.SignalLogs,
Filter: &qbtypes.Filter{Expression: "trace_id EXISTS"},
Limit: 10,
}
stmt, err := statementBuilder.Build(context.Background(), start, end, qbtypes.RequestTypeRaw, q, nil)
require.NoError(t, err)
n := execLogCount(t, ts, *stmt)
t.Logf("log trace_id exists rows=%d\nSQL: %s\nARGS: %v", n, stmt.Query, stmt.Args)
})
}