aggregate_spans
Calculate latency percentiles, error rates, and request counts across spans to identify performance bottlenecks and debug application issues.
Instructions
Calculate aggregated metrics and statistics across spans.
Use this tool to:
Get latency percentiles for endpoints (p50, p95, p99)
Calculate error rates by endpoint
Get request counts over time
Compare performance across environments
Examples:
Endpoint latency: groupBy = ["name"], metrics = ["count", "avgDuration", "p95Duration"]
Error rates: groupBy = ["name"], metrics = ["count", "errorCount", "errorRate"]
Hourly trends: timeBucket = "hour", metrics = ["count", "errorRate"]
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| observableServiceId | No | Service ID to query (required if multiple services available) | |
| where | No | Filter conditions | |
| groupBy | No | Fields to group by | |
| metrics | Yes | Metrics to calculate | |
| timeBucket | No | Time bucket for time-series data | |
| orderBy | No | Order by metric | |
| limit | No | Max results |
Implementation Reference
- src/tools/aggregateSpans.ts:11-76 (schema)Tool definition for aggregate_spans, including its name, description, and inputSchema (JSON Schema for the MCP tool). Defines parameters: observableServiceId, where, groupBy, metrics, timeBucket, orderBy, limit.
export const aggregateSpansTool: Tool = { name: "aggregate_spans", description: `Calculate aggregated metrics and statistics across spans. Use this tool to: - Get latency percentiles for endpoints (p50, p95, p99) - Calculate error rates by endpoint - Get request counts over time - Compare performance across environments Examples: - Endpoint latency: groupBy = ["name"], metrics = ["count", "avgDuration", "p95Duration"] - Error rates: groupBy = ["name"], metrics = ["count", "errorCount", "errorRate"] - Hourly trends: timeBucket = "hour", metrics = ["count", "errorRate"]`, inputSchema: { type: "object", properties: { observableServiceId: { type: "string", description: "Service ID to query. Required if multiple services are available.", }, where: { type: "object", description: "Filter conditions (same as query_spans)", }, groupBy: { type: "array", description: "Fields to group by", items: { type: "string", enum: [...aggregateGroupFieldCodec.names], }, }, metrics: { type: "array", description: "Metrics to calculate", items: { type: "string", enum: [...aggregateMetricCodec.names], }, }, timeBucket: { type: "string", description: "Time bucket for time-series data", enum: [...timeBucketCodec.names], }, orderBy: { type: "object", description: "Order results by a metric", properties: { metric: { type: "string", enum: [...aggregateMetricCodec.names], }, direction: { type: "string", enum: [...sortDirectionCodec.names] }, }, }, limit: { type: "number", description: "Maximum results to return", default: 20, }, }, required: ["metrics"], }, }; - src/tools/aggregateSpans.ts:78-118 (handler)Handler function handleAggregateSpans that parses input, calls client.aggregateSpans(), and formats the result rows with group values and metrics (count, errorCount, errorRate, avgDuration, minDuration, maxDuration, p50Duration, p95Duration, p99Duration) into a human-readable text response.
export async function handleAggregateSpans( client: TuskDriftApiClient, args: Record<string, unknown> ): Promise<{ content: Array<{ type: "text"; text: string }> }> { const input = parseAggregateSpansInput(args); const result = await client.aggregateSpans(input); const header = `Aggregation Results (${result.results.length} rows):\n`; const rows = result.results .map((row, i) => { const groupStr = Object.entries(row.groupValues) .map(([k, v]) => `${k}=${v}`) .join(", "); const metrics: string[] = []; if (row.count !== undefined) metrics.push(`count: ${row.count}`); if (row.errorCount !== undefined) metrics.push(`errors: ${row.errorCount}`); if (row.errorRate !== undefined) metrics.push(`error rate: ${(row.errorRate * 100).toFixed(2)}%`); if (row.avgDuration !== undefined) metrics.push(`avg: ${row.avgDuration.toFixed(2)}ms`); if (row.minDuration !== undefined) metrics.push(`min: ${row.minDuration.toFixed(2)}ms`); if (row.maxDuration !== undefined) metrics.push(`max: ${row.maxDuration.toFixed(2)}ms`); if (row.p50Duration !== undefined) metrics.push(`p50: ${row.p50Duration.toFixed(2)}ms`); if (row.p95Duration !== undefined) metrics.push(`p95: ${row.p95Duration.toFixed(2)}ms`); if (row.p99Duration !== undefined) metrics.push(`p99: ${row.p99Duration.toFixed(2)}ms`); const timeBucketStr = row.timeBucket ? ` [${row.timeBucket}]` : ""; return `${i + 1}. ${groupStr || "(all)"}${timeBucketStr}\n ${metrics.join(" | ")}`; }) .join("\n\n"); return { content: [ { type: "text", text: header + rows, }, ], }; } - src/tools/index.ts:24-31 (registration)Registration of aggregate_spans tool handler in the toolHandlers record, mapping the name 'aggregate_spans' to the handleAggregateSpans function. Also registered in the tools array at line 14.
export const toolHandlers: Record<string, ToolHandler> = { query_spans: handleQuerySpans, get_schema: handleGetSchema, list_distinct_values: handleListDistinctValues, aggregate_spans: handleAggregateSpans, get_trace: handleGetTrace, get_spans_by_ids: handleGetSpansByIds, }; - src/types.ts:247-267 (schema)Zod validation schema (aggregateSpansInputSchema) for aggregate_spans input: observableServiceId (optional), where (optional filter), groupBy (optional array), metrics (required array, min 1), timeBucket (optional), orderBy (optional), limit (optional, default 20, max 100).
export const aggregateSpansInputSchema = z.object({ observableServiceId: z.string().optional().describe("Service ID to query (required if multiple services available)"), where: spanWhereClauseSchema.optional().describe("Filter conditions"), groupBy: z .array(enumNameSchema(aggregateGroupFieldCodec)) .optional() .describe("Fields to group by"), metrics: z .array(enumNameSchema(aggregateMetricCodec)) .min(1) .describe("Metrics to calculate"), timeBucket: enumNameSchema(timeBucketCodec).optional().describe("Time bucket for time-series data"), orderBy: z .object({ metric: enumNameSchema(aggregateMetricCodec), direction: enumNameSchema(sortDirectionCodec), }) .optional() .describe("Order by metric"), limit: z.number().min(1).max(100).default(20).describe("Max results"), }); - src/types.ts:387-403 (helper)parseAggregateSpansInput function that validates raw args using the Zod schema and converts them to the protobuf-based AggregateSpansInput (SharedAggregateSpansRequest), mapping enum names to enum values for groupBy, metrics, timeBucket, and orderBy.
export function parseAggregateSpansInput(args: Record<string, unknown>): AggregateSpansInput { const input: AggregateSpansArgs = aggregateSpansInputSchema.parse(args); return SharedAggregateSpansRequest.create({ observableServiceId: input.observableServiceId ?? "", where: input.where ? toProtoWhereClause(input.where) : undefined, groupBy: (input.groupBy ?? []).map((field) => aggregateGroupFieldCodec.byName[field]), metrics: input.metrics.map((metric) => aggregateMetricCodec.byName[metric]), timeBucket: input.timeBucket ? timeBucketCodec.byName[input.timeBucket] : TimeBucket.UNSPECIFIED, orderBy: input.orderBy ? { metric: aggregateMetricCodec.byName[input.orderBy.metric], direction: sortDirectionCodec.byName[input.orderBy.direction], } : undefined, limit: input.limit, }); }