#9548·jaeger

[Bug]: get_critical_path MCP tool corrupts trace offsets when a span has an unset (zero) StartTimestamp

Author: Atul-KoundalCreated Sep 13, 2026Updated Sep 15, 2026
Labelsbug

What happened?

The get_critical_path MCP tool (used by AI agents/clients to inspect the critical latency path of a trace) can return badly wrong timing offsets for every segment in a trace, whenever the trace contains at least one span with an unset/zero StartTimestamp (e.g. malformed data or a partially-adjusted span).

The handler computes the trace's overall start time by scanning all spans and tracking the minimum start timestamp, using 0 as a sentinel meaning "no start time seen yet." A span whose StartTimestamp is genuinely zero is indistinguishable from that sentinel. If such a span happens to be iterated before a span with a real, later start time, the real span's start time silently overwrites the correct trace start time of 0.

Because the affected fields (StartOffsetUs, EndOffsetUs) are unsigned integers computed as sectionTime - traceStartTime, once traceStartTime is wrongly inflated this way, the subtraction underflows instead of going negative producing a nonsensical offset near 2^64 instead of a small, correct number. This is returned directly to whatever MCP client/agent called the tool, with no error or warning.

This is order-dependent: it only manifests when the zero-timestamp span happens to be processed before the real spans, since span iteration order (ResourceSpans -> ScopeSpans -> Spans) comes from however the storage backend returned the data, not something the caller controls.

Steps to reproduce

This requires a trace containing a span with an unset/zero StartTimestamp ordered before a span with a real timestamp. Minimal isolated repro of the exact logic in buildOutput() (cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/internal/handlers/get_critical_path.go):

starts := []uint64{0, 1_000_000} // zero-timestamp span, then a real span 1s later

var traceStartTime uint64
for _, startTime := range starts {
    if traceStartTime == 0 || startTime < traceStartTime {
        traceStartTime = startTime
    }
}
// traceStartTime ends up 1_000_000, but the correct minimum is 0

Concretely via the MCP tool:

  1. Have a trace where one span never had StartTimestamp set (zero value) and appears first in storage iteration order, alongside a normal span with a real StartTimestamp.
  2. Call the get_critical_path MCP tool for that trace ID.
  3. Inspect StartOffsetUs/EndOffsetUs on the returned segments.

Expected behavior

traceStartTime should equal the true minimum StartTimestamp across all spans in the trace, including a legitimate value of 0, regardless of which order spans are iterated in. StartOffsetUs/EndOffsetUs should always be small, correct, non-underflowed values relative to the trace's actual start.

Relevant log output

bash
No log output : this is a silent data-correctness bug, not a crash or logged error. The returned StartOffsetUs/EndOffsetUs values are simply wrong (potentially near-2^64 due to uint64 underflow).

Screenshot

No response

Additional context

Root cause is in buildOutput() in get_critical_path.go:

var traceStartTime uint64
...
if traceStartTime == 0 || startTime < traceStartTime {
    traceStartTime = startTime
}

Suggested fix: track initialization with an explicit boolean instead of overloading 0 as a sentinel:

var traceStartTime uint64
haveStartTime := false
...
if !haveStartTime || startTime < traceStartTime {
    traceStartTime = startTime
    haveStartTime = true
}

(traceEndTime's existing endTime > traceEndTime check does not have this problem, since it doesn't special-case 0.)

I have a patch with this fix plus a regression test (TestGetCriticalPathHandler_BuildOutput_ZeroStartTimestampSpan) ready to submit as a PR against this issue : happy to open it if this bug report is accepted. I verified the corrected logic in isolation but couldn't run the full test suite in my environment (only had Go 1.22 available; go.mod pins go 1.27.0), so I'd want CI/a maintainer to confirm go test ./cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/internal/handlers/... passes before merging.

Found via manual code review of cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/internal/handlers/, not via a live deployment.

Jaeger backend version

No response

SDK

No response

Pipeline

No response

Stogage backend

No response

Operating system

No response

Deployment model

No response

Deployment configs

bash