From 45497b338d2edb7d7265198df229ea2f5a12dcae Mon Sep 17 00:00:00 2001 From: szibis <329831+szibis@users.noreply.github.com> Date: Fri, 15 May 2026 00:24:45 +0200 Subject: [PATCH 1/2] fix: Tempo search API returns incorrect startTimeUnixNano and overflows durationMs When a span row lacks the start_time_unix_nano field (e.g., trace index rows), the local variable defaults to 0. min(MaxInt64, 0) produces 0, corrupting the trace summary's start time. This causes durationMs to equal endTimeUnixNano/1e6 (~1.78 trillion), which overflows Grafana's uint32 durationMs field with: "json: cannot unmarshal number into Go value of type uint32". Fix: only update startTimeUnixNano/endTimeUnixNano when the field is actually present in the span row. Also quote startTimeUnixNano as a string in the JSON response to match the Tempo API specification (Grafana Tempo returns startTimeUnixNano as a quoted string, not a bare integer). Verified against Grafana Tempo's protobuf spec (tempo.proto) and HTTP API documentation examples. --- app/vtselect/traces/tempo/tempo.go | 14 +- app/vtselect/traces/tempo/tempo.qtpl | 2 +- app/vtselect/traces/tempo/tempo.qtpl.go | 282 ++++++++++++------------ 3 files changed, 154 insertions(+), 144 deletions(-) diff --git a/app/vtselect/traces/tempo/tempo.go b/app/vtselect/traces/tempo/tempo.go index 5f0a517c9..9a9799148 100644 --- a/app/vtselect/traces/tempo/tempo.go +++ b/app/vtselect/traces/tempo/tempo.go @@ -428,6 +428,7 @@ func summarySearchTracesResult(ctx context.Context, rows []*tracecommon.Row, lim for _, row := range rows { var traceID, serviceName, spanName, parentSpanID string var startTimeUnixNano, endTimeUnixNano int64 + var hasStart, hasEnd bool var err error for _, field := range row.Fields { switch field.Name { @@ -444,11 +445,13 @@ func summarySearchTracesResult(ctx context.Context, rows []*tracecommon.Row, lim if err != nil { return nil, err } + hasStart = true case otelpb.EndTimeUnixNanoField: endTimeUnixNano, err = strconv.ParseInt(field.Value, 10, 64) if err != nil { return nil, err } + hasEnd = true default: continue } @@ -469,8 +472,12 @@ func summarySearchTracesResult(ctx context.Context, rows []*tracecommon.Row, lim } summary.traceID = traceID - summary.startTimeUnixNano = min(summary.startTimeUnixNano, startTimeUnixNano) - summary.endTimeUnixNano = max(summary.endTimeUnixNano, endTimeUnixNano) + if hasStart { + summary.startTimeUnixNano = min(summary.startTimeUnixNano, startTimeUnixNano) + } + if hasEnd { + summary.endTimeUnixNano = max(summary.endTimeUnixNano, endTimeUnixNano) + } // if it's the root span if parentSpanID == "" { summary.rootServiceName = serviceName @@ -482,6 +489,9 @@ func summarySearchTracesResult(ctx context.Context, rows []*tracecommon.Row, lim resultList := make([]traceSummary, 0, len(traceMap)) for _, summary := range traceMap { + if summary.startTimeUnixNano == math.MaxInt64 { + summary.startTimeUnixNano = 0 + } resultList = append(resultList, summary) } return resultList, nil diff --git a/app/vtselect/traces/tempo/tempo.qtpl b/app/vtselect/traces/tempo/tempo.qtpl index 955f63477..d6452832e 100644 --- a/app/vtselect/traces/tempo/tempo.qtpl +++ b/app/vtselect/traces/tempo/tempo.qtpl @@ -121,7 +121,7 @@ "traceID":{%q= summary.traceID %}, "rootServiceName":{%q= summary.rootServiceName %}, "rootTraceName":{%q= summary.rootTraceName %}, - "startTimeUnixNano":{%dl= summary.startTimeUnixNano %}, + "startTimeUnixNano":"{%dl= summary.startTimeUnixNano %}", "durationMs":{%dl= (summary.endTimeUnixNano - summary.startTimeUnixNano) / 1e6 %}, "spanSets": [] } diff --git a/app/vtselect/traces/tempo/tempo.qtpl.go b/app/vtselect/traces/tempo/tempo.qtpl.go index be003bd97..6ef52e068 100644 --- a/app/vtselect/traces/tempo/tempo.qtpl.go +++ b/app/vtselect/traces/tempo/tempo.qtpl.go @@ -1,311 +1,311 @@ // Code generated by qtc from "tempo.qtpl". DO NOT EDIT. // See https://github.com/valyala/quicktemplate for details. -//line tempo.qtpl:1 +//line app/vtselect/traces/tempo/tempo.qtpl:1 package tempo -//line tempo.qtpl:1 +//line app/vtselect/traces/tempo/tempo.qtpl:1 import ( "sort" ) -//line tempo.qtpl:7 +//line app/vtselect/traces/tempo/tempo.qtpl:7 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line tempo.qtpl:7 +//line app/vtselect/traces/tempo/tempo.qtpl:7 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line tempo.qtpl:7 +//line app/vtselect/traces/tempo/tempo.qtpl:7 func StreamSearchTagsResponse(qw422016 *qt422016.Writer, resourceTagList, spanTagList, eventTagList, linkTagList, instrumentationScopeTagList []string) { -//line tempo.qtpl:7 +//line app/vtselect/traces/tempo/tempo.qtpl:7 qw422016.N().S(`{`) -//line tempo.qtpl:10 +//line app/vtselect/traces/tempo/tempo.qtpl:10 sort.Slice(resourceTagList, func(i, j int) bool { return resourceTagList[i] < resourceTagList[j] }) sort.Slice(spanTagList, func(i, j int) bool { return spanTagList[i] < spanTagList[j] }) sort.Slice(eventTagList, func(i, j int) bool { return eventTagList[i] < eventTagList[j] }) sort.Slice(linkTagList, func(i, j int) bool { return linkTagList[i] < linkTagList[j] }) sort.Slice(instrumentationScopeTagList, func(i, j int) bool { return instrumentationScopeTagList[i] < instrumentationScopeTagList[j] }) -//line tempo.qtpl:15 +//line app/vtselect/traces/tempo/tempo.qtpl:15 qw422016.N().S(`"scopes":[{"name": "resource","tags": [`) -//line tempo.qtpl:20 +//line app/vtselect/traces/tempo/tempo.qtpl:20 if len(resourceTagList) > 0 { -//line tempo.qtpl:21 +//line app/vtselect/traces/tempo/tempo.qtpl:21 qw422016.N().Q(resourceTagList[0]) -//line tempo.qtpl:22 +//line app/vtselect/traces/tempo/tempo.qtpl:22 for _, tag := range resourceTagList[1:] { -//line tempo.qtpl:22 +//line app/vtselect/traces/tempo/tempo.qtpl:22 qw422016.N().S(`,`) -//line tempo.qtpl:23 +//line app/vtselect/traces/tempo/tempo.qtpl:23 qw422016.N().Q(tag) -//line tempo.qtpl:24 +//line app/vtselect/traces/tempo/tempo.qtpl:24 } -//line tempo.qtpl:25 +//line app/vtselect/traces/tempo/tempo.qtpl:25 } -//line tempo.qtpl:25 +//line app/vtselect/traces/tempo/tempo.qtpl:25 qw422016.N().S(`]},{"name": "span","tags": [`) -//line tempo.qtpl:31 +//line app/vtselect/traces/tempo/tempo.qtpl:31 if len(spanTagList) > 0 { -//line tempo.qtpl:32 +//line app/vtselect/traces/tempo/tempo.qtpl:32 qw422016.N().Q(spanTagList[0]) -//line tempo.qtpl:33 +//line app/vtselect/traces/tempo/tempo.qtpl:33 for _, tag := range spanTagList[1:] { -//line tempo.qtpl:33 +//line app/vtselect/traces/tempo/tempo.qtpl:33 qw422016.N().S(`,`) -//line tempo.qtpl:34 +//line app/vtselect/traces/tempo/tempo.qtpl:34 qw422016.N().Q(tag) -//line tempo.qtpl:35 +//line app/vtselect/traces/tempo/tempo.qtpl:35 } -//line tempo.qtpl:36 +//line app/vtselect/traces/tempo/tempo.qtpl:36 } -//line tempo.qtpl:36 +//line app/vtselect/traces/tempo/tempo.qtpl:36 qw422016.N().S(`]},{"name": "event","tags": [`) -//line tempo.qtpl:42 +//line app/vtselect/traces/tempo/tempo.qtpl:42 if len(eventTagList) > 0 { -//line tempo.qtpl:43 +//line app/vtselect/traces/tempo/tempo.qtpl:43 qw422016.N().Q(eventTagList[0]) -//line tempo.qtpl:44 +//line app/vtselect/traces/tempo/tempo.qtpl:44 for _, tag := range eventTagList[1:] { -//line tempo.qtpl:44 +//line app/vtselect/traces/tempo/tempo.qtpl:44 qw422016.N().S(`,`) -//line tempo.qtpl:45 +//line app/vtselect/traces/tempo/tempo.qtpl:45 qw422016.N().Q(tag) -//line tempo.qtpl:46 +//line app/vtselect/traces/tempo/tempo.qtpl:46 } -//line tempo.qtpl:47 +//line app/vtselect/traces/tempo/tempo.qtpl:47 } -//line tempo.qtpl:47 +//line app/vtselect/traces/tempo/tempo.qtpl:47 qw422016.N().S(`]},{"name": "link","tags": [`) -//line tempo.qtpl:53 +//line app/vtselect/traces/tempo/tempo.qtpl:53 if len(linkTagList) > 0 { -//line tempo.qtpl:54 +//line app/vtselect/traces/tempo/tempo.qtpl:54 qw422016.N().Q(linkTagList[0]) -//line tempo.qtpl:55 +//line app/vtselect/traces/tempo/tempo.qtpl:55 for _, tag := range linkTagList[1:] { -//line tempo.qtpl:55 +//line app/vtselect/traces/tempo/tempo.qtpl:55 qw422016.N().S(`,`) -//line tempo.qtpl:56 +//line app/vtselect/traces/tempo/tempo.qtpl:56 qw422016.N().Q(tag) -//line tempo.qtpl:57 +//line app/vtselect/traces/tempo/tempo.qtpl:57 } -//line tempo.qtpl:58 +//line app/vtselect/traces/tempo/tempo.qtpl:58 } -//line tempo.qtpl:58 +//line app/vtselect/traces/tempo/tempo.qtpl:58 qw422016.N().S(`]},{"name": "instrumentation","tags": [`) -//line tempo.qtpl:64 +//line app/vtselect/traces/tempo/tempo.qtpl:64 if len(instrumentationScopeTagList) > 0 { -//line tempo.qtpl:65 +//line app/vtselect/traces/tempo/tempo.qtpl:65 qw422016.N().Q(instrumentationScopeTagList[0]) -//line tempo.qtpl:66 +//line app/vtselect/traces/tempo/tempo.qtpl:66 for _, tag := range instrumentationScopeTagList[1:] { -//line tempo.qtpl:66 +//line app/vtselect/traces/tempo/tempo.qtpl:66 qw422016.N().S(`,`) -//line tempo.qtpl:67 +//line app/vtselect/traces/tempo/tempo.qtpl:67 qw422016.N().Q(tag) -//line tempo.qtpl:68 +//line app/vtselect/traces/tempo/tempo.qtpl:68 } -//line tempo.qtpl:69 +//line app/vtselect/traces/tempo/tempo.qtpl:69 } -//line tempo.qtpl:69 +//line app/vtselect/traces/tempo/tempo.qtpl:69 qw422016.N().S(`]}],"metrics": {"inspectedBytes": "0"}}`) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 } -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 func WriteSearchTagsResponse(qq422016 qtio422016.Writer, resourceTagList, spanTagList, eventTagList, linkTagList, instrumentationScopeTagList []string) { -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 qw422016 := qt422016.AcquireWriter(qq422016) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 StreamSearchTagsResponse(qw422016, resourceTagList, spanTagList, eventTagList, linkTagList, instrumentationScopeTagList) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 qt422016.ReleaseWriter(qw422016) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 } -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 func SearchTagsResponse(resourceTagList, spanTagList, eventTagList, linkTagList, instrumentationScopeTagList []string) string { -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 qb422016 := qt422016.AcquireByteBuffer() -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 WriteSearchTagsResponse(qb422016, resourceTagList, spanTagList, eventTagList, linkTagList, instrumentationScopeTagList) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 qs422016 := string(qb422016.B) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 qt422016.ReleaseByteBuffer(qb422016) -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 return qs422016 -//line tempo.qtpl:77 +//line app/vtselect/traces/tempo/tempo.qtpl:77 } -//line tempo.qtpl:79 +//line app/vtselect/traces/tempo/tempo.qtpl:79 func StreamSearchTagValuesResponse(qw422016 *qt422016.Writer, tagValueList []string) { -//line tempo.qtpl:79 +//line app/vtselect/traces/tempo/tempo.qtpl:79 qw422016.N().S(`{`) -//line tempo.qtpl:82 +//line app/vtselect/traces/tempo/tempo.qtpl:82 sort.Slice(tagValueList, func(i, j int) bool { return tagValueList[i] < tagValueList[j] }) -//line tempo.qtpl:83 +//line app/vtselect/traces/tempo/tempo.qtpl:83 qw422016.N().S(`"tagValues":[`) -//line tempo.qtpl:85 +//line app/vtselect/traces/tempo/tempo.qtpl:85 if len(tagValueList) > 0 { -//line tempo.qtpl:85 +//line app/vtselect/traces/tempo/tempo.qtpl:85 qw422016.N().S(`{"type": "string","value":`) -//line tempo.qtpl:88 +//line app/vtselect/traces/tempo/tempo.qtpl:88 qw422016.N().Q(tagValueList[0]) -//line tempo.qtpl:88 +//line app/vtselect/traces/tempo/tempo.qtpl:88 qw422016.N().S(`}`) -//line tempo.qtpl:90 +//line app/vtselect/traces/tempo/tempo.qtpl:90 } -//line tempo.qtpl:91 +//line app/vtselect/traces/tempo/tempo.qtpl:91 if len(tagValueList) > 1 { -//line tempo.qtpl:92 +//line app/vtselect/traces/tempo/tempo.qtpl:92 for _, tag := range tagValueList[1:] { -//line tempo.qtpl:92 +//line app/vtselect/traces/tempo/tempo.qtpl:92 qw422016.N().S(`,{"type": "string","value":`) -//line tempo.qtpl:95 +//line app/vtselect/traces/tempo/tempo.qtpl:95 qw422016.N().Q(tag) -//line tempo.qtpl:95 +//line app/vtselect/traces/tempo/tempo.qtpl:95 qw422016.N().S(`}`) -//line tempo.qtpl:97 +//line app/vtselect/traces/tempo/tempo.qtpl:97 } -//line tempo.qtpl:98 +//line app/vtselect/traces/tempo/tempo.qtpl:98 } -//line tempo.qtpl:98 +//line app/vtselect/traces/tempo/tempo.qtpl:98 qw422016.N().S(`],"metrics": {"inspectedBytes": "0"}}`) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 } -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 func WriteSearchTagValuesResponse(qq422016 qtio422016.Writer, tagValueList []string) { -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 qw422016 := qt422016.AcquireWriter(qq422016) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 StreamSearchTagValuesResponse(qw422016, tagValueList) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 qt422016.ReleaseWriter(qw422016) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 } -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 func SearchTagValuesResponse(tagValueList []string) string { -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 qb422016 := qt422016.AcquireByteBuffer() -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 WriteSearchTagValuesResponse(qb422016, tagValueList) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 qs422016 := string(qb422016.B) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 qt422016.ReleaseByteBuffer(qb422016) -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 return qs422016 -//line tempo.qtpl:104 +//line app/vtselect/traces/tempo/tempo.qtpl:104 } -//line tempo.qtpl:106 +//line app/vtselect/traces/tempo/tempo.qtpl:106 func StreamSearchResponse(qw422016 *qt422016.Writer, summaryList []traceSummary) { -//line tempo.qtpl:106 +//line app/vtselect/traces/tempo/tempo.qtpl:106 qw422016.N().S(`{"traces": [`) -//line tempo.qtpl:109 +//line app/vtselect/traces/tempo/tempo.qtpl:109 if len(summaryList) > 0 { -//line tempo.qtpl:110 +//line app/vtselect/traces/tempo/tempo.qtpl:110 streamsummaryJson(qw422016, summaryList[0]) -//line tempo.qtpl:111 +//line app/vtselect/traces/tempo/tempo.qtpl:111 for _, summary := range summaryList[1:] { -//line tempo.qtpl:111 +//line app/vtselect/traces/tempo/tempo.qtpl:111 qw422016.N().S(`,`) -//line tempo.qtpl:112 +//line app/vtselect/traces/tempo/tempo.qtpl:112 streamsummaryJson(qw422016, summary) -//line tempo.qtpl:113 +//line app/vtselect/traces/tempo/tempo.qtpl:113 } -//line tempo.qtpl:114 +//line app/vtselect/traces/tempo/tempo.qtpl:114 } -//line tempo.qtpl:114 +//line app/vtselect/traces/tempo/tempo.qtpl:114 qw422016.N().S(`]}`) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 } -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 func WriteSearchResponse(qq422016 qtio422016.Writer, summaryList []traceSummary) { -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 qw422016 := qt422016.AcquireWriter(qq422016) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 StreamSearchResponse(qw422016, summaryList) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 qt422016.ReleaseWriter(qw422016) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 } -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 func SearchResponse(summaryList []traceSummary) string { -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 qb422016 := qt422016.AcquireByteBuffer() -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 WriteSearchResponse(qb422016, summaryList) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 qs422016 := string(qb422016.B) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 qt422016.ReleaseByteBuffer(qb422016) -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 return qs422016 -//line tempo.qtpl:117 +//line app/vtselect/traces/tempo/tempo.qtpl:117 } -//line tempo.qtpl:119 +//line app/vtselect/traces/tempo/tempo.qtpl:119 func streamsummaryJson(qw422016 *qt422016.Writer, summary traceSummary) { -//line tempo.qtpl:119 +//line app/vtselect/traces/tempo/tempo.qtpl:119 qw422016.N().S(`{"traceID":`) -//line tempo.qtpl:121 +//line app/vtselect/traces/tempo/tempo.qtpl:121 qw422016.N().Q(summary.traceID) -//line tempo.qtpl:121 +//line app/vtselect/traces/tempo/tempo.qtpl:121 qw422016.N().S(`,"rootServiceName":`) -//line tempo.qtpl:122 +//line app/vtselect/traces/tempo/tempo.qtpl:122 qw422016.N().Q(summary.rootServiceName) -//line tempo.qtpl:122 +//line app/vtselect/traces/tempo/tempo.qtpl:122 qw422016.N().S(`,"rootTraceName":`) -//line tempo.qtpl:123 +//line app/vtselect/traces/tempo/tempo.qtpl:123 qw422016.N().Q(summary.rootTraceName) -//line tempo.qtpl:123 - qw422016.N().S(`,"startTimeUnixNano":`) -//line tempo.qtpl:124 +//line app/vtselect/traces/tempo/tempo.qtpl:123 + qw422016.N().S(`,"startTimeUnixNano":"`) +//line app/vtselect/traces/tempo/tempo.qtpl:124 qw422016.N().DL(summary.startTimeUnixNano) -//line tempo.qtpl:124 - qw422016.N().S(`,"durationMs":`) -//line tempo.qtpl:125 +//line app/vtselect/traces/tempo/tempo.qtpl:124 + qw422016.N().S(`","durationMs":`) +//line app/vtselect/traces/tempo/tempo.qtpl:125 qw422016.N().DL((summary.endTimeUnixNano - summary.startTimeUnixNano) / 1e6) -//line tempo.qtpl:125 +//line app/vtselect/traces/tempo/tempo.qtpl:125 qw422016.N().S(`,"spanSets": []}`) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 } -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 func writesummaryJson(qq422016 qtio422016.Writer, summary traceSummary) { -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 qw422016 := qt422016.AcquireWriter(qq422016) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 streamsummaryJson(qw422016, summary) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 qt422016.ReleaseWriter(qw422016) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 } -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 func summaryJson(summary traceSummary) string { -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 qb422016 := qt422016.AcquireByteBuffer() -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 writesummaryJson(qb422016, summary) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 qs422016 := string(qb422016.B) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 qt422016.ReleaseByteBuffer(qb422016) -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 return qs422016 -//line tempo.qtpl:128 +//line app/vtselect/traces/tempo/tempo.qtpl:128 } From 8f7e5350c2c263bd2f8922741a502eea53e4bea8 Mon Sep 17 00:00:00 2001 From: szibis <329831+szibis@users.noreply.github.com> Date: Fri, 15 May 2026 00:26:29 +0200 Subject: [PATCH 2/2] test: add tests for Tempo search startTimeUnixNano and durationMs Tests cover: - Single span with all fields - Span missing start_time_unix_nano (the bug trigger) - All spans missing start time (sentinel fallback to 0) - Multiple traces - Missing trace_id error - Root span identification - durationMs fits uint32 (Tempo API compat) - JSON response format (startTimeUnixNano quoted as string) --- app/vtselect/traces/tempo/tempo_test.go | 301 ++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 app/vtselect/traces/tempo/tempo_test.go diff --git a/app/vtselect/traces/tempo/tempo_test.go b/app/vtselect/traces/tempo/tempo_test.go new file mode 100644 index 000000000..2b34d4dcb --- /dev/null +++ b/app/vtselect/traces/tempo/tempo_test.go @@ -0,0 +1,301 @@ +package tempo + +import ( + "context" + "math" + "strings" + "testing" + + "github.com/VictoriaMetrics/VictoriaLogs/lib/logstorage" + + "github.com/VictoriaMetrics/VictoriaTraces/app/vtselect/traces/tracecommon" + otelpb "github.com/VictoriaMetrics/VictoriaTraces/lib/protoparser/opentelemetry/pb" +) + +func TestSummarySearchTracesResult(t *testing.T) { + tests := []struct { + name string + rows []*tracecommon.Row + wantTraceIDs []string + wantStartNano int64 + wantEndNano int64 + wantServiceName string + wantTraceName string + wantErr bool + }{ + { + name: "single span with all fields", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc123"}, + {Name: otelpb.ResourceAttrServiceName, Value: "my-service"}, + {Name: otelpb.NameField, Value: "GET /api"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.StartTimeUnixNanoField, Value: "1000000000000"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "1000032000000"}, + }, + }, + }, + wantTraceIDs: []string{"abc123"}, + wantStartNano: 1000000000000, + wantEndNano: 1000032000000, + wantServiceName: "my-service", + wantTraceName: "GET /api", + }, + { + name: "span missing start_time_unix_nano should not corrupt startTime", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc123"}, + {Name: otelpb.ResourceAttrServiceName, Value: "my-service"}, + {Name: otelpb.NameField, Value: "GET /api"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.EndTimeUnixNanoField, Value: "1000032000000"}, + }, + }, + { + Timestamp: 2000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc123"}, + {Name: otelpb.StartTimeUnixNanoField, Value: "1000000000000"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "1000020000000"}, + {Name: otelpb.ParentSpanIDField, Value: "abc123span1"}, + }, + }, + }, + wantTraceIDs: []string{"abc123"}, + wantStartNano: 1000000000000, + wantEndNano: 1000032000000, + wantServiceName: "my-service", + wantTraceName: "GET /api", + }, + { + name: "all spans missing start_time should produce 0 not MaxInt64", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc123"}, + {Name: otelpb.ResourceAttrServiceName, Value: "svc"}, + {Name: otelpb.NameField, Value: "op"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.EndTimeUnixNanoField, Value: "5000000000000"}, + }, + }, + }, + wantTraceIDs: []string{"abc123"}, + wantStartNano: 0, + wantEndNano: 5000000000000, + wantServiceName: "svc", + wantTraceName: "op", + }, + { + name: "multiple traces", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "trace1"}, + {Name: otelpb.ResourceAttrServiceName, Value: "svc-a"}, + {Name: otelpb.NameField, Value: "op-a"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.StartTimeUnixNanoField, Value: "100"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "200"}, + }, + }, + { + Timestamp: 2000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "trace2"}, + {Name: otelpb.ResourceAttrServiceName, Value: "svc-b"}, + {Name: otelpb.NameField, Value: "op-b"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.StartTimeUnixNanoField, Value: "300"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "400"}, + }, + }, + }, + wantTraceIDs: []string{"trace1", "trace2"}, + }, + { + name: "missing trace_id returns error", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.StartTimeUnixNanoField, Value: "100"}, + }, + }, + }, + wantErr: true, + }, + { + name: "root span identified by empty parent_span_id", + rows: []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "t1"}, + {Name: otelpb.ParentSpanIDField, Value: "parent1"}, + {Name: otelpb.ResourceAttrServiceName, Value: "child-svc"}, + {Name: otelpb.NameField, Value: "child-op"}, + {Name: otelpb.StartTimeUnixNanoField, Value: "200"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "300"}, + }, + }, + { + Timestamp: 2000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "t1"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.ResourceAttrServiceName, Value: "root-svc"}, + {Name: otelpb.NameField, Value: "root-op"}, + {Name: otelpb.StartTimeUnixNanoField, Value: "100"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "350"}, + }, + }, + }, + wantTraceIDs: []string{"t1"}, + wantStartNano: 100, + wantEndNano: 350, + wantServiceName: "root-svc", + wantTraceName: "root-op", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result, err := summarySearchTracesResult(context.Background(), tc.rows, 100) + if tc.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(tc.wantTraceIDs) > 0 { + gotIDs := make(map[string]bool) + for _, s := range result { + gotIDs[s.traceID] = true + } + for _, wantID := range tc.wantTraceIDs { + if !gotIDs[wantID] { + t.Errorf("missing trace ID %q in result", wantID) + } + } + } + + if len(tc.wantTraceIDs) == 1 && len(result) == 1 { + s := result[0] + if tc.wantStartNano != 0 || tc.wantEndNano != 0 { + if s.startTimeUnixNano != tc.wantStartNano { + t.Errorf("startTimeUnixNano = %d, want %d", s.startTimeUnixNano, tc.wantStartNano) + } + if s.endTimeUnixNano != tc.wantEndNano { + t.Errorf("endTimeUnixNano = %d, want %d", s.endTimeUnixNano, tc.wantEndNano) + } + } + if tc.wantServiceName != "" && s.rootServiceName != tc.wantServiceName { + t.Errorf("rootServiceName = %q, want %q", s.rootServiceName, tc.wantServiceName) + } + if tc.wantTraceName != "" && s.rootTraceName != tc.wantTraceName { + t.Errorf("rootTraceName = %q, want %q", s.rootTraceName, tc.wantTraceName) + } + } + + // Verify no summary has startTimeUnixNano == math.MaxInt64 (sentinel leak) + for _, s := range result { + if s.startTimeUnixNano == math.MaxInt64 { + t.Errorf("trace %q has sentinel startTimeUnixNano (math.MaxInt64), should be 0", s.traceID) + } + } + }) + } +} + +func TestSummarySearchDurationMsFitsUint32(t *testing.T) { + // Reproduce the original bug: a span without start_time_unix_nano + // caused durationMs to be ~1.78 trillion, overflowing uint32. + rows := []*tracecommon.Row{ + { + Timestamp: 1000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc"}, + {Name: otelpb.ParentSpanIDField, Value: ""}, + {Name: otelpb.ResourceAttrServiceName, Value: "svc"}, + {Name: otelpb.NameField, Value: "op"}, + // No start_time_unix_nano — this is the bug trigger + {Name: otelpb.EndTimeUnixNanoField, Value: "1778797098296000000"}, + }, + }, + { + Timestamp: 2000, + Fields: []logstorage.Field{ + {Name: otelpb.TraceIDField, Value: "abc"}, + {Name: otelpb.ParentSpanIDField, Value: "span1"}, + {Name: otelpb.StartTimeUnixNanoField, Value: "1778797098262000000"}, + {Name: otelpb.EndTimeUnixNanoField, Value: "1778797098294000000"}, + }, + }, + } + + result, err := summarySearchTracesResult(context.Background(), rows, 100) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) != 1 { + t.Fatalf("expected 1 trace, got %d", len(result)) + } + + s := result[0] + durationMs := (s.endTimeUnixNano - s.startTimeUnixNano) / 1e6 + + // durationMs must fit in uint32 (max ~4.2 billion) for Tempo API compat + if durationMs > math.MaxUint32 { + t.Errorf("durationMs = %d, exceeds uint32 max (%d); startTimeUnixNano=%d, endTimeUnixNano=%d", + durationMs, uint32(math.MaxUint32), s.startTimeUnixNano, s.endTimeUnixNano) + } + + if s.startTimeUnixNano == 0 { + t.Errorf("startTimeUnixNano = 0, should be 1778797098262000000 (from the span that has it)") + } +} + +func TestSearchResponseJSON(t *testing.T) { + summaries := []traceSummary{ + { + traceID: "abc123", + rootServiceName: "my-service", + rootTraceName: "GET /api", + startTimeUnixNano: 1684778327699392724, + endTimeUnixNano: 1684778327756392724, + }, + } + + json := SearchResponse(summaries) + + // startTimeUnixNano must be a quoted string per Tempo API spec + if !strings.Contains(json, `"startTimeUnixNano":"1684778327699392724"`) { + t.Errorf("startTimeUnixNano should be a quoted string, got: %s", json) + } + + // durationMs should be a bare integer + if !strings.Contains(json, `"durationMs":57`) { + t.Errorf("durationMs should be bare integer 57, got: %s", json) + } + + // Basic structure checks + if !strings.Contains(json, `"traceID":"abc123"`) { + t.Errorf("missing traceID in response: %s", json) + } + if !strings.Contains(json, `"rootServiceName":"my-service"`) { + t.Errorf("missing rootServiceName in response: %s", json) + } +}