Files
zeekay b9f9cdf918 feat: bump o11y fork to latest SigNoz — full re-sync, builds green (§4c retained)
Re-sync the entire SigNoz-derived Go tree to a clean fork of latest SigNoz
main (upstream 3e6339019), eliminating the version-skew that broke
`go build ./...` (mixed-era packages, ~14 imported-but-absent packages,
partial signoz->o11y / clickhouse->datastore rebrands, self-import cycle,
duplicate package decls).

- pkg/ + cmd/ wholesale replaced with latest SigNoz; imports rewritten
  github.com/SigNoz/signoz/ -> github.com/hanzoai/o11y/.
- Adopt SigNoz's CURRENT structure (resourcefilter->telemetryresourcefilter,
  integrationtypes->cloudintegrationtypes, roletypes/grammar/times folded in
  upstream); old paths dropped.
- Fork deps repathed to hanzoai + bumped: signoz-otel-collector v0.144.6,
  clickhouse-go-mock v0.14.1, govaluate v0.1.0; expr-lang/expr => hanzoai/expr
  v1.17.8 (replace). go.mod reconciled from latest SigNoz; go.sum re-tidied.
- ee/ + cmd/enterprise stripped — o11y stays MIT-only; community edition
  imports no ee packages.
- Preserved o11y additions: module path github.com/hanzoai/o11y, mount.go
  (cloud.Register/zip mount adapter, HIP-0106), NOTICE + LICENSE (SigNoz §4c
  attribution retained; upstream pkg files are headerless per SigNoz convention).
- Reverted o11y cosmetic renames (o11y*/datastore*) and experimental originals
  (authz/iamauthz, billing, zapreceiver, zapmetricreceiver) to SigNoz canonical
  to kill the skew; snapshots retained in git history for re-layering.

go build ./... = exit 0. go vet clean except benign "unreachable code" in
generated ANTLR parser files (inherited from upstream).
2026-07-02 14:20:32 -07:00

575 lines
22 KiB
Go

// Package impltracedetail tests — waterfall
//
// # Background
//
// The waterfall view renders a trace as a scrollable list of spans in
// pre-order (parent before children, siblings left-to-right). Because a trace
// can have thousands of spans, only a window of ~500 is returned per request.
// The window is centred on the selected span.
//
// # Key concepts
//
// uncollapsedSpans
//
// The set of span IDs the user has manually expanded in the UI.
// Only the direct children of an uncollapsed span are included in the
// output; grandchildren stay hidden until their parent is also uncollapsed.
// When multiple spans are uncollapsed their children are all visible at once.
//
// selectedSpanID
//
// The span currently focused — set when the user clicks a span in the
// waterfall or selects one from the flamegraph. The output window is always
// centred on this span. The path from the trace root down to the selected
// span is automatically uncollapsed so ancestors are visible even if they are
// not in uncollapsedSpans.
//
//
// traceRoots
//
// Root spans of the trace — spans with no parent in the current dataset.
// Normally one, but multiple roots are common when upstream services are
// not instrumented or their spans were not sampled/exported.
package impltracedetail
import (
"fmt"
"testing"
"github.com/hanzoai/o11y/pkg/types/spantypes"
"github.com/stretchr/testify/assert"
)
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
func mkSpan(id, service string, children ...*spantypes.WaterfallSpan) *spantypes.WaterfallSpan {
return &spantypes.WaterfallSpan{
SpanID: id,
ServiceName: service,
Name: id + "-op",
Children: children,
}
}
func spanIDs(spans []*spantypes.WaterfallSpan) []string {
ids := make([]string, len(spans))
for i, s := range spans {
ids[i] = s.SpanID
}
return ids
}
func buildSpanMap(roots ...*spantypes.WaterfallSpan) map[string]*spantypes.WaterfallSpan {
m := map[string]*spantypes.WaterfallSpan{}
var walk func(*spantypes.WaterfallSpan)
walk = func(s *spantypes.WaterfallSpan) {
m[s.SpanID] = s
for _, c := range s.Children {
walk(c)
}
}
for _, r := range roots {
r.SortChildren()
r.GetSubtreeNodeCount()
walk(r)
}
return m
}
// makeChain builds a linear trace: span0 → span1 → … → span(n-1).
func makeChain(n int) (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan, []string) {
spans := make([]*spantypes.WaterfallSpan, n)
for i := n - 1; i >= 0; i-- {
if i == n-1 {
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc")
} else {
spans[i] = mkSpan(fmt.Sprintf("span%d", i), "svc", spans[i+1])
}
}
uncollapsed := make([]string, n)
for i := range spans {
uncollapsed[i] = fmt.Sprintf("span%d", i)
}
return spans[0], buildSpanMap(spans[0]), uncollapsed
}
func getWaterfallTrace(roots []*spantypes.WaterfallSpan, spanMap map[string]*spantypes.WaterfallSpan) *spantypes.WaterfallTrace {
return spantypes.NewWaterfallTrace(0, 0, uint64(len(spanMap)), 0, spanMap, roots, false)
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — span ordering and visibility
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_SpanOrdering(t *testing.T) {
tests := []struct {
name string
buildRoots func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan)
uncollapsedSpans []string
selectedSpanID string
wantSpanIDs []string
}{
{
// Pre-order traversal is preserved: parent before children, siblings left-to-right.
name: "pre_order_traversal",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
mkSpan("child2", "svc"),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"root", "child1"},
selectedSpanID: "root",
wantSpanIDs: []string{"root", "child1", "grandchild", "child2"},
},
{
// Multiple spans uncollapsed simultaneously: children of all uncollapsed spans are visible at once.
//
// root
// ├─ childA (uncollapsed) → grandchildA ✓
// └─ childB (uncollapsed) → grandchildB ✓
name: "multiple_uncollapsed",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc", mkSpan("grandchildA", "svc")),
mkSpan("childB", "svc", mkSpan("grandchildB", "svc")),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"root", "childA", "childB"},
selectedSpanID: "root",
wantSpanIDs: []string{"root", "childA", "grandchildA", "childB", "grandchildB"},
},
{
// Collapsing a span with other uncollapsed spans.
//
// root
// ├─ childA (previously expanded — in uncollapsedSpans)
// │ ├─ grandchild1 ✓
// │ │ └─ greatGrandchild ✗ (grandchild1 not in uncollapsedSpans)
// │ └─ grandchild2 ✓
// └─ childB ← selected (not expanded)
name: "manual_uncollapse",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchild1", "svc", mkSpan("greatGrandchild", "svc")),
mkSpan("grandchild2", "svc"),
),
mkSpan("childB", "svc"),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{"childA"},
selectedSpanID: "childB",
wantSpanIDs: []string{"root", "childA", "grandchild1", "grandchild2", "childB"},
},
{
// A collapsed span hides all children.
name: "collapsed_span_hides_children",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc"),
mkSpan("child2", "svc"),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "root",
wantSpanIDs: []string{"root"},
},
{
// Selecting a span auto-uncollpases the path from root to that span so it is visible.
//
// root → parent → selected
name: "path_to_selected_is_uncollapsed",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected"},
},
{
// Siblings of ancestors are rendered as collapsed nodes but their subtrees must NOT be expanded.
//
// root
// ├─ unrelated → unrelated-child (✗)
// └─ parent → selected
name: "siblings_not_expanded",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
// children of root sort alphabetically: parent < unrelated; unrelated-child stays hidden
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
},
{
// An unknown selectedSpanID must not panic; returns a window from index 0.
name: "unknown_selected_span",
buildRoots: func() ([]*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc", mkSpan("child", "svc"))
return []*spantypes.WaterfallSpan{root}, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "nonexistent",
wantSpanIDs: []string{"root"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
roots, spanMap := tc.buildRoots()
trace := getWaterfallTrace(roots, spanMap)
spans, _ := trace.GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, 500, 5)
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
})
}
}
// Multiple roots: both trees are flattened into a single pre-order list with
// root1's subtree before root2's. Service/entry-point come from the first root.
//
// root1 svc-a ← selected
// └─ child1
// root2 svc-b
// └─ child2
//
// Expected output order: root1 → child1 → root2 → child2.
func TestGetSelectedSpans_MultipleRoots(t *testing.T) {
root1 := mkSpan("root1", "svc-a", mkSpan("child1", "svc-a"))
root2 := mkSpan("root2", "svc-b", mkSpan("child2", "svc-b"))
spanMap := buildSpanMap(root1, root2)
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root1, root2}, spanMap)
spans, _ := trace.GetSelectedSpans([]string{"root1", "root2"}, "root1", 500, 5)
traceRespnose := spantypes.NewGettableWaterfallTrace(trace, spans, nil, false)
assert.Equal(t, []string{"root1", "child1", "root2", "child2"}, spanIDs(spans), "root1 subtree must precede root2 subtree")
assert.Equal(t, "svc-a", traceRespnose.RootServiceName, "metadata comes from first root")
assert.Equal(t, "root1-op", traceRespnose.RootServiceEntryPoint, "metadata comes from first root")
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — uncollapsed span tracking
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_UncollapsedTracking(t *testing.T) {
tests := []struct {
name string
buildRoot func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan)
uncollapsedSpans []string
selectedSpanID string
wantSpanIDs []string
checkUncollapsed func(t *testing.T, uncollapsed []string)
}{
{
// The path-to-selected spans are returned in updatedUncollapsedSpans.
name: "path_returned_in_uncollapsed",
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected"},
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
},
},
{
// Siblings of ancestors are not tracked as uncollapsed.
name: "siblings_not_in_uncollapsed",
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("unrelated", "svc", mkSpan("unrelated-child", "svc")),
mkSpan("parent", "svc",
mkSpan("selected", "svc"),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{},
selectedSpanID: "selected",
wantSpanIDs: []string{"root", "parent", "selected", "unrelated"},
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.ElementsMatch(t, []string{"root", "parent"}, uncollapsed)
},
},
{
// Auto-expanded span IDs from ALL branches are returned in updatedUncollapsedSpans.
// Only internal nodes (spans with children) are tracked — leaf spans are never added.
// root is in uncollapsedSpans, so its children are auto-expanded.
//
// root (selected, expanded)
// ├─ childA (internal ✓)
// │ └─ grandchildA (internal ✓)
// │ └─ leafA (leaf ✗)
// └─ childB (internal ✓)
// └─ grandchildB (internal ✓)
// └─ leafB (leaf ✗)
name: "auto_expanded_spans_returned",
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchildA", "svc",
mkSpan("leafA", "svc"),
),
),
mkSpan("childB", "svc",
mkSpan("grandchildB", "svc",
mkSpan("leafB", "svc"),
),
),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{"root"},
selectedSpanID: "root",
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
assert.Contains(t, uncollapsed, "root")
assert.Contains(t, uncollapsed, "childA", "internal node depth 1, branch A")
assert.Contains(t, uncollapsed, "childB", "internal node depth 1, branch B")
assert.Contains(t, uncollapsed, "grandchildA", "internal node depth 2, branch A")
assert.Contains(t, uncollapsed, "grandchildB", "internal node depth 2, branch B")
assert.NotContains(t, uncollapsed, "leafA", "leaf spans are never added to uncollapsedSpans")
assert.NotContains(t, uncollapsed, "leafB", "leaf spans are never added to uncollapsedSpans")
},
},
{
// If the selected span is already in uncollapsedSpans,
// it should appear exactly once in the result.
name: "duplicate_in_uncollapsed",
buildRoot: func() (*spantypes.WaterfallSpan, map[string]*spantypes.WaterfallSpan) {
root := mkSpan("root", "svc",
mkSpan("selected", "svc", mkSpan("child", "svc")),
)
return root, buildSpanMap(root)
},
uncollapsedSpans: []string{"selected"}, // already present
selectedSpanID: "selected",
checkUncollapsed: func(t *testing.T, uncollapsed []string) {
count := 0
for _, id := range uncollapsed {
if id == "selected" {
count++
}
}
assert.Equal(t, 1, count, "should appear once")
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root, spanMap := tc.buildRoot()
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
spans, uncollapsed := trace.GetSelectedSpans(tc.uncollapsedSpans, tc.selectedSpanID, 500, 5)
if tc.wantSpanIDs != nil {
assert.Equal(t, tc.wantSpanIDs, spanIDs(spans))
}
if tc.checkUncollapsed != nil {
tc.checkUncollapsed(t, uncollapsed)
}
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — span metadata
// ─────────────────────────────────────────────────────────────────────────────
// Test to check if Level, HasChildren, and SubTreeNodeCount are populated correctly.
//
// root level=0, hasChildren=true, subTree=4
// child1 level=1, hasChildren=true, subTree=2
// grandchild level=2, hasChildren=false, subTree=1
// child2 level=1, hasChildren=false, subTree=1
func TestGetSelectedSpans_SpanMetadata(t *testing.T) {
root := mkSpan("root", "svc",
mkSpan("child1", "svc", mkSpan("grandchild", "svc")),
mkSpan("child2", "svc"),
)
spanMap := buildSpanMap(root)
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
spans, _ := trace.GetSelectedSpans([]string{"root", "child1"}, "root", 500, 5)
byID := map[string]*spantypes.WaterfallSpan{}
for _, s := range spans {
byID[s.SpanID] = s
}
tests := []struct {
spanID string
wantLevel uint64
wantHasChildren bool
wantSubTree uint64
}{
{"root", 0, true, 4},
{"child1", 1, true, 2},
{"child2", 1, false, 1},
{"grandchild", 2, false, 1},
}
for _, tc := range tests {
t.Run(tc.spanID, func(t *testing.T) {
s := byID[tc.spanID]
assert.Equal(t, tc.wantLevel, s.Level)
assert.Equal(t, tc.wantHasChildren, s.HasChildren)
assert.Equal(t, tc.wantSubTree, s.SubTreeNodeCount)
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — windowing
// ─────────────────────────────────────────────────────────────────────────────
func TestGetSelectedSpans_Window(t *testing.T) {
tests := []struct {
name string
selectedSpanID string
wantLen int
wantFirst string
wantLast string
wantSelectedPos int
}{
{
// The selected span is centred: 200 spans before it, 300 after (0.4 / 0.6 split).
name: "centred_on_selected",
selectedSpanID: "span300",
wantLen: 500,
wantFirst: "span100",
wantLast: "span599",
wantSelectedPos: 200,
},
{
// When the selected span is near the start, the window shifts right so no
// negative index is used — the result is still 500 spans.
name: "shifts_at_start",
selectedSpanID: "span10",
wantLen: 500,
wantFirst: "span0",
wantSelectedPos: 10,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
root, spanMap, uncollapsed := makeChain(600)
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
spans, _ := trace.GetSelectedSpans(uncollapsed, tc.selectedSpanID, 500, 5)
assert.Equal(t, tc.wantLen, len(spans), "window size")
assert.Equal(t, tc.wantFirst, spans[0].SpanID, "first span in window")
if tc.wantLast != "" {
assert.Equal(t, tc.wantLast, spans[len(spans)-1].SpanID, "last span in window")
}
assert.Equal(t, tc.selectedSpanID, spans[tc.wantSelectedPos].SpanID, "selected span position")
})
}
}
// ─────────────────────────────────────────────────────────────────────────────
// GetSelectedSpans — depth limit
// ─────────────────────────────────────────────────────────────────────────────
// Depth is measured from the selected span, not the trace root.
// Ancestors appear via the path-to-root logic, not the depth limit.
// Each depth level has two children to confirm the limit is enforced on all
// branches, not just the first.
//
// root
// └─ A ancestor ✓ (path-to-root)
// └─ selected
// ├─ d1a depth 1 ✓
// │ ├─ d2a depth 2 ✓
// │ │ ├─ d3a depth 3 ✓
// │ │ │ ├─ d4a depth 4 ✓
// │ │ │ │ ├─ d5a depth 5 ✓
// │ │ │ │ │ └─ d6a depth 6 ✗
// │ │ │ │ └─ d5b depth 5 ✓
// │ │ │ └─ d4b depth 4 ✓
// │ │ └─ d3b depth 3 ✓
// │ └─ d2b depth 2 ✓
// └─ d1b depth 1 ✓
func TestGetSelectedSpans_DepthCountedFromSelectedSpan(t *testing.T) {
selected := mkSpan("selected", "svc",
mkSpan("d1a", "svc",
mkSpan("d2a", "svc",
mkSpan("d3a", "svc",
mkSpan("d4a", "svc",
mkSpan("d5a", "svc",
mkSpan("d6a", "svc"), // depth 6 — excluded
),
mkSpan("d5b", "svc"), // depth 5 — included
),
mkSpan("d4b", "svc"), // depth 4 — included
),
mkSpan("d3b", "svc"), // depth 3 — included
),
mkSpan("d2b", "svc"), // depth 2 — included
),
mkSpan("d1b", "svc"), // depth 1 — included
)
root := mkSpan("root", "svc", mkSpan("A", "svc", selected))
spanMap := buildSpanMap(root)
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, spanMap)
spans, _ := trace.GetSelectedSpans([]string{"selected"}, "selected", 500, 5)
ids := spanIDs(spans)
assert.Contains(t, ids, "root", "ancestor shown via path-to-root")
assert.Contains(t, ids, "A", "ancestor shown via path-to-root")
for _, id := range []string{"d1a", "d1b", "d2a", "d2b", "d3a", "d3b", "d4a", "d4b", "d5a", "d5b"} {
assert.Contains(t, ids, id, "depth ≤ 5 — must be included")
}
assert.NotContains(t, ids, "d6a", "depth 6 > limit — excluded")
}
// ─────────────────────────────────────────────────────────────────────────────
// GetAllSpans
// ─────────────────────────────────────────────────────────────────────────────
func TestGetAllSpans(t *testing.T) {
root := mkSpan("root", "svc",
mkSpan("childA", "svc",
mkSpan("grandchildA", "svc",
mkSpan("leafA", "svc2"),
),
),
mkSpan("childB", "svc3",
mkSpan("grandchildB", "svc",
mkSpan("leafB", "svc2"),
),
),
)
trace := getWaterfallTrace([]*spantypes.WaterfallSpan{root}, nil)
spans := trace.GetAllSpans()
traceResponse := spantypes.NewGettableWaterfallTrace(trace, spans, nil, true)
assert.ElementsMatch(t, spanIDs(spans), []string{"root", "childA", "grandchildA", "leafA", "childB", "grandchildB", "leafB"})
assert.Equal(t, "svc", traceResponse.RootServiceName)
assert.Equal(t, "root-op", traceResponse.RootServiceEntryPoint)
}