Skip to content

Commit e0ba1a6

Browse files
authored
[jaegermcp] Expose service names in search_traces trace summaries (#8339)
## Which problem is this PR solving? When an agent calls `search_traces`, the response includes `service_count` (an integer) but not the actual service names. The agent knows a trace spans 5 services but has no idea which ones. To find out, it must call `get_trace_topology` or `get_span_details` for every trace individually, which defeats the purpose of a lightweight summary endpoint. The data is already computed internally. `buildTraceSummary` builds a `services` map to derive `service_count`, then discards the map keys. This has been the case since the function was introduced in #7858, where the map was built but only `len(services)` was surfaced. Subsequent changes (#7859, #7863, #7916, #8194) restructured the types and renamed fields but never revisited the service data gap. ## Short description of the changes - Added `Services []string` to `TraceSummary`, populated from the existing `services` map - Sorted alphabetically via `slices.Sort` for deterministic output across calls - Added multi-service test with three services in non-alphabetical order, unique span IDs, and proper parent-child relationships to verify sort correctness - Updated existing summary tests to assert on the new field - `ServiceCount` preserved for backward compatibility ## Use case An agent investigating a latency spike searches for slow traces. The summary now returns: ```json { "service_count": 3, "services": ["api-gateway", "payment", "user-service"] } ``` The agent can immediately see that the payment service is involved and drill into that trace, instead of blindly fetching topology for every result. ## How was this change tested? - `go test ./cmd/jaeger/internal/extension/jaegermcp/...` - all passing - `make lint` - 0 issues - `make fmt` - clean - `make test` - 3053 tests passing ## Checklist - [x] I have read https://github.com/jaegertracing/jaeger/blob/main/CONTRIBUTING_GUIDELINES.md - [x] I have signed all commits - [x] I have added unit tests for the new functionality - [x] I have run lint and test steps successfully: `make lint test` ## AI Usage in this PR (choose one) - [x] **Light**: AI provided minor assistance (formatting, simple suggestions) Signed-off-by: Roshan Singh <roshansingh7890@gmail.com> Signed-off-by: Roshan <rosh.s568@gmail.com>
1 parent 7cccec2 commit e0ba1a6

3 files changed

Lines changed: 53 additions & 8 deletions

File tree

cmd/jaeger/internal/extension/jaegermcp/internal/handlers/search_traces.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"iter"
11+
"slices"
1112
"strings"
1213
"time"
1314

@@ -235,12 +236,19 @@ func buildTraceSummary(trace ptrace.Traces) types.TraceSummary {
235236
summary.RootService = rootServiceName
236237
}
237238

239+
serviceNames := make([]string, 0, len(services))
240+
for svc := range services {
241+
serviceNames = append(serviceNames, svc)
242+
}
243+
slices.Sort(serviceNames)
244+
238245
// Build summary
239246
summary.TraceID = traceID.String()
240247
summary.StartTime = minStartTime.Format(time.RFC3339)
241248
summary.DurationUs = duration.Microseconds()
242249
summary.SpanCount = spanCount
243250
summary.ServiceCount = len(services)
251+
summary.Services = serviceNames
244252
summary.HasErrors = hasErrors
245253

246254
return summary

cmd/jaeger/internal/extension/jaegermcp/internal/handlers/search_traces_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/modelcontextprotocol/go-sdk/mcp"
1414
"github.com/stretchr/testify/assert"
1515
"github.com/stretchr/testify/require"
16+
"go.opentelemetry.io/collector/pdata/pcommon"
1617
"go.opentelemetry.io/collector/pdata/ptrace"
1718

1819
"github.com/jaegertracing/jaeger/cmd/jaeger/internal/extension/jaegermcp/internal/types"
@@ -33,6 +34,7 @@ func TestSearchTracesHandler_Handle_Success(t *testing.T) {
3334
assert.Equal(t, "/api/checkout", summary.RootSpanName)
3435
assert.Equal(t, 1, summary.SpanCount)
3536
assert.Equal(t, 1, summary.ServiceCount)
37+
assert.Equal(t, []string{"frontend"}, summary.Services)
3638
assert.False(t, summary.HasErrors)
3739
}
3840

@@ -43,9 +45,43 @@ func TestSearchTracesHandler_BuildSummary_WithErrors(t *testing.T) {
4345

4446
assert.Equal(t, "payment", summary.RootService)
4547
assert.Equal(t, "/process", summary.RootSpanName)
48+
assert.Equal(t, []string{"payment"}, summary.Services)
4649
assert.True(t, summary.HasErrors)
4750
}
4851

52+
func TestSearchTracesHandler_BuildSummary_MultipleServices(t *testing.T) {
53+
traces := ptrace.NewTraces()
54+
tid := pcommon.TraceID{}
55+
copy(tid[:], "multiservicetrace")
56+
57+
// Add spans from three different services in non-alphabetical order
58+
spanIDs := []pcommon.SpanID{
59+
{1, 0, 0, 0, 0, 0, 0, 0},
60+
{2, 0, 0, 0, 0, 0, 0, 0},
61+
{3, 0, 0, 0, 0, 0, 0, 0},
62+
}
63+
for i, svc := range []string{"payment", "api-gateway", "user-service"} {
64+
rs := traces.ResourceSpans().AppendEmpty()
65+
rs.Resource().Attributes().PutStr("service.name", svc)
66+
span := rs.ScopeSpans().AppendEmpty().Spans().AppendEmpty()
67+
span.SetTraceID(tid)
68+
span.SetSpanID(spanIDs[i])
69+
span.SetName("/op")
70+
span.SetStartTimestamp(pcommon.NewTimestampFromTime(time.Now().Add(-5 * time.Second)))
71+
span.SetEndTimestamp(pcommon.NewTimestampFromTime(time.Now()))
72+
span.Status().SetCode(ptrace.StatusCodeOk)
73+
if i > 0 {
74+
span.SetParentSpanID(spanIDs[0])
75+
}
76+
}
77+
78+
summary := buildTraceSummary(traces)
79+
80+
assert.Equal(t, 3, summary.ServiceCount)
81+
// Services must be sorted alphabetically for deterministic output
82+
assert.Equal(t, []string{"api-gateway", "payment", "user-service"}, summary.Services)
83+
}
84+
4985
func TestSearchTracesHandler_Handle_FullWorkflow(t *testing.T) {
5086
testTrace := createTestTrace("trace789", "cart-service", "/get-cart", false)
5187

cmd/jaeger/internal/extension/jaegermcp/internal/types/search_traces.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,13 @@ type SearchTracesOutput struct {
4747

4848
// TraceSummary contains lightweight metadata about a single trace.
4949
type TraceSummary struct {
50-
TraceID string `json:"trace_id" jsonschema:"Unique identifier for the trace"`
51-
RootService string `json:"root_service" jsonschema:"Service name of the root span"`
52-
RootSpanName string `json:"root_span_name" jsonschema:"Span name of the root span"`
53-
StartTime string `json:"start_time" jsonschema:"Trace start time in RFC3339 format"`
54-
DurationUs int64 `json:"duration_us" jsonschema:"Total trace duration in microseconds"`
55-
SpanCount int `json:"span_count" jsonschema:"Total number of spans in the trace"`
56-
ServiceCount int `json:"service_count" jsonschema:"Number of unique services in the trace"`
57-
HasErrors bool `json:"has_errors" jsonschema:"Whether the trace contains any error spans"`
50+
TraceID string `json:"trace_id" jsonschema:"Unique identifier for the trace"`
51+
RootService string `json:"root_service" jsonschema:"Service name of the root span"`
52+
RootSpanName string `json:"root_span_name" jsonschema:"Span name of the root span"`
53+
StartTime string `json:"start_time" jsonschema:"Trace start time in RFC3339 format"`
54+
DurationUs int64 `json:"duration_us" jsonschema:"Total trace duration in microseconds"`
55+
SpanCount int `json:"span_count" jsonschema:"Total number of spans in the trace"`
56+
ServiceCount int `json:"service_count" jsonschema:"Number of unique services in the trace"`
57+
Services []string `json:"services" jsonschema:"Sorted list of unique service names participating in the trace"`
58+
HasErrors bool `json:"has_errors" jsonschema:"Whether the trace contains any error spans"`
5859
}

0 commit comments

Comments
 (0)