gcp-trace-query-natural-language
Query Google Cloud trace data using natural language to find specific insights, such as failed traces or recent activity, across your project's logs efficiently.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectId | No | Optional Google Cloud project ID | |
| query | Yes | Natural language query about traces (e.g., "Show me failed traces from the last hour") |
Implementation Reference
- src/services/trace/tools.ts:757-1283 (handler)Primary handler for gcp-trace-query-natural-language tool. Parses natural language input to query traces: supports specific trace ID lookup, filtered lists (errors, time ranges), and log-based trace discovery. Integrates GCP Cloud Trace API v1 and logging.server.tool( "gcp-trace-query-natural-language", { query: z .string() .describe( 'Natural language query about traces (e.g., "Show me failed traces from the last hour")', ), projectId: z .string() .optional() .describe("Optional Google Cloud project ID"), }, async ({ query, projectId }, context) => { try { // Use provided project ID or get the default one from state manager first const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); // Process the natural language query const normalizedQuery = query.toLowerCase(); // Default parameters let filter = ""; let limit = 10; let startTime = "1h"; // Default to 1 hour let traceId = ""; // Extract trace ID if present const traceIdMatch = normalizedQuery.match( /trace(?:\s+id)?[:\s]+([a-f0-9]+)/i, ); if (traceIdMatch && traceIdMatch[1]) { traceId = traceIdMatch[1]; // If we have a trace ID, implement the get-trace functionality directly // Validate trace ID format (hex string) if (typeof traceId === "string" && !traceId.match(/^[a-f0-9]+$/i)) { throw new GcpMcpError( "Invalid trace ID format. Trace ID should be a hexadecimal string.", "INVALID_ARGUMENT", 400, ); } // Initialize Google Auth client const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( "Google Cloud authentication not available. Please configure authentication to access trace data.", "UNAUTHENTICATED", 401, ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Fetch the trace from the Cloud Trace API const response = await fetch( `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`, { headers: { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", }, }, ); if (!response.ok) { const errorText = await response.text(); throw new GcpMcpError( `Failed to fetch trace: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } const traceData = await response.json(); if (!traceData || !traceData.spans || traceData.spans.length === 0) { return { content: [ { type: "text", text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`, }, ], }; } // Build the trace hierarchy const hierarchicalTrace = buildTraceHierarchy( actualProjectId.toString(), traceId.toString(), traceData.spans, ); // Format the trace data for display const formattedTrace = formatTraceData(hierarchicalTrace); return { content: [ { type: "text", text: formattedTrace, }, ], }; } // Extract time range if ( normalizedQuery.includes("last hour") || normalizedQuery.includes("past hour") ) { startTime = "1h"; } else if ( normalizedQuery.includes("last day") || normalizedQuery.includes("past day") || normalizedQuery.includes("24 hours") || normalizedQuery.includes("today") ) { startTime = "24h"; } else if ( normalizedQuery.includes("last week") || normalizedQuery.includes("past week") ) { startTime = "7d"; } else if ( normalizedQuery.includes("last month") || normalizedQuery.includes("past month") ) { startTime = "30d"; } // Extract status filter if ( normalizedQuery.includes("fail") || normalizedQuery.includes("error") || normalizedQuery.includes("exception") || normalizedQuery.includes("problem") ) { filter = "status.code != 0"; // Non-zero status codes indicate errors } // Extract limit const limitMatch = normalizedQuery.match( /\b(show|display|list|get|find)?\s+(\d+)\s+(trace|span|result)/i, ); if (limitMatch && limitMatch[2]) { limit = parseInt(limitMatch[2]); limit = Math.min(Math.max(limit, 1), 100); // Clamp between 1 and 100 } // If query mentions logs, use the find-traces-from-logs tool if ( normalizedQuery.includes("log") || normalizedQuery.includes("logging") ) { let logFilter = "severity>=ERROR"; // Default to error logs if ( normalizedQuery.includes("info") || normalizedQuery.includes("information") ) { logFilter = "severity>=INFO"; } else if ( normalizedQuery.includes("warn") || normalizedQuery.includes("warning") ) { logFilter = "severity>=WARNING"; } else if (normalizedQuery.includes("debug")) { logFilter = "severity>=DEBUG"; } // Implement find-traces-from-logs functionality directly // Initialize the logging client const logging = new Logging({ projectId: actualProjectId, }); // Fetch logs with the given filter const [entries] = await logging.getEntries({ filter: logFilter, pageSize: limit, }); if (!entries || entries.length === 0) { return { content: [ { type: "text", text: `No logs found matching the filter: "${logFilter}" in project: ${actualProjectId}`, }, ], }; } // Extract trace IDs from logs const traceMap = new Map< string, { traceId: string; timestamp: string; severity: string; logName: string; message: string; } >(); for (const entry of entries) { const metadata = entry.metadata; const traceId = extractTraceIdFromLog(metadata); if (traceId) { // Convert timestamp to string let timestampStr = "Unknown"; if (metadata.timestamp) { if (typeof metadata.timestamp === "string") { timestampStr = metadata.timestamp; } else { try { // Handle different timestamp formats if ( typeof metadata.timestamp === "object" && metadata.timestamp !== null ) { if ( "seconds" in metadata.timestamp && "nanos" in metadata.timestamp ) { // Handle Timestamp proto format const seconds = Number(metadata.timestamp.seconds); const nanos = Number(metadata.timestamp.nanos || 0); const milliseconds = seconds * 1000 + nanos / 1000000; timestampStr = new Date(milliseconds).toISOString(); } else { // Try to convert using JSON timestampStr = JSON.stringify(metadata.timestamp); } } else { timestampStr = String(metadata.timestamp); } } catch (e) { timestampStr = "Invalid timestamp"; } } } // Convert severity to string let severityStr = "DEFAULT"; if (metadata.severity) { severityStr = String(metadata.severity); } // Convert logName to string let logNameStr = "Unknown"; if (metadata.logName) { logNameStr = String(metadata.logName); } // Extract message let messageStr = "No message"; if (metadata.textPayload) { messageStr = String(metadata.textPayload); } else if (metadata.jsonPayload) { try { messageStr = JSON.stringify(metadata.jsonPayload); } catch (e) { messageStr = "Invalid JSON payload"; } } traceMap.set(traceId, { traceId, timestamp: timestampStr, severity: severityStr, logName: logNameStr, message: messageStr, }); } } if (traceMap.size === 0) { return { content: [ { type: "text", text: `No traces found in the logs matching the filter: "${logFilter}" in project: ${actualProjectId}`, }, ], }; } // Format the traces for display let markdown = `# Traces Found in Logs\n\n`; markdown += `Project: ${actualProjectId}\n`; markdown += `Log Filter: ${logFilter}\n`; markdown += `Found ${traceMap.size} unique traces in ${entries.length} log entries:\n\n`; // Table header markdown += "| Trace ID | Timestamp | Severity | Log Name | Message |\n"; markdown += "|----------|-----------|----------|----------|--------|\n"; // Table rows for (const trace of traceMap.values()) { const traceId = trace.traceId; // Handle timestamp formatting safely let timestamp = trace.timestamp; try { if ( timestamp !== "Unknown" && timestamp !== "Invalid timestamp" ) { timestamp = new Date(trace.timestamp).toISOString(); } } catch (e) { // Keep the original timestamp if conversion fails } const severity = trace.severity; const logName = trace.logName.split("/").pop() || trace.logName; const message = trace.message.length > 100 ? `${trace.message.substring(0, 100)}...` : trace.message; markdown += `| ${traceId} | ${timestamp} | ${severity} | ${logName} | ${message} |\n`; } return { content: [ { type: "text", text: markdown, }, ], }; } // Otherwise, use the list-traces tool // Implement list-traces functionality directly // Calculate time range const endTime = new Date(); let startTimeDate: Date; if (startTime) { logger.debug(`Raw startTime parameter: ${JSON.stringify(startTime)}`); // Handle the case where startTime might be passed as an object from JSON const startTimeStr = typeof startTime === "string" ? startTime : String(startTime); logger.debug(`Processing startTime: ${startTimeStr}`); // Check if the input is a relative time format (e.g., "1h", "2d", "30m") if (startTimeStr.match(/^\d+[hmd]$/)) { // Parse relative time (e.g., "1h", "2d") const value = parseInt(startTimeStr.slice(0, -1)); const unit = startTimeStr.slice(-1); startTimeDate = new Date(endTime); if (unit === "h") { startTimeDate.setHours(startTimeDate.getHours() - value); } else if (unit === "d") { startTimeDate.setDate(startTimeDate.getDate() - value); } else if (unit === "m") { startTimeDate.setMinutes(startTimeDate.getMinutes() - value); } logger.debug( `Parsed relative time: ${startTimeStr} to ${startTimeDate.toISOString()}`, ); } else { // Parse ISO format try { startTimeDate = new Date(startTimeStr); if (isNaN(startTimeDate.getTime())) { throw new Error("Invalid date format"); } logger.debug( `Parsed ISO time: ${startTimeStr} to ${startTimeDate.toISOString()}`, ); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; logger.error(`Error parsing time: ${errorMessage}`); throw new GcpMcpError( `Invalid start time format: "${startTimeStr}". Use ISO format or relative time (e.g., "1h", "2d").`, "INVALID_ARGUMENT", 400, ); } } } else { // Default to 1 hour ago startTimeDate = new Date(endTime); startTimeDate.setHours(startTimeDate.getHours() - 1); } // Build the request body const requestBody: any = { projectId: actualProjectId, startTime: startTimeDate.toISOString(), endTime: endTime.toISOString(), pageSize: limit, }; if (filter) { requestBody.filter = filter; } // Initialize Google Auth client const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( "Google Cloud authentication not available. Please configure authentication to access trace data.", "UNAUTHENTICATED", 401, ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Build the query parameters for the request const queryParams = new URLSearchParams(); // Format timestamps in RFC3339 UTC "Zulu" format const startTimeUTC = new Date( startTimeDate.toISOString(), ).toISOString(); const endTimeUTC = new Date(endTime.toISOString()).toISOString(); // Add required query parameters according to the API documentation queryParams.append("startTime", startTimeUTC); queryParams.append("endTime", endTimeUTC); queryParams.append("pageSize", limit.toString()); // Add view parameter to specify the type of data returned queryParams.append("view", "MINIMAL"); if (filter) { queryParams.append("filter", filter); } // Construct the URL for the Cloud Trace API v1 endpoint const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`; const requestUrl = `${apiUrl}?${queryParams.toString()}`; logger.debug(`Fetching traces from: ${requestUrl}`); // Fetch traces from the Cloud Trace API const response = await fetch(requestUrl, { method: "GET", headers: { Authorization: `Bearer ${token.token}`, Accept: "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new GcpMcpError( `Failed to list traces: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } const tracesData = await response.json(); // Check if we have valid traces data // In v1 API, the response contains a traces array if (!tracesData.traces || tracesData.traces.length === 0) { return { content: [ { type: "text", text: `No traces found matching the criteria in project: ${actualProjectId}`, }, ], }; } // Format the traces for display let markdown = `# Traces for ${actualProjectId}\n\n`; markdown += `Time Range: ${startTimeDate.toISOString()} to ${endTime.toISOString()}\n`; markdown += `Filter: ${filter || "None"}\n\n`; markdown += `Found ${tracesData.traces.length} traces:\n\n`; // Table header markdown += "| Trace ID | Display Name | Start Time | Duration | Status |\n"; markdown += "|----------|--------------|------------|----------|--------|\n"; // Table rows for (const trace of tracesData.traces) { const traceId = trace.traceId; const displayName = trace.displayName || "Unknown"; const startTimeStr = new Date(trace.startTime).toISOString(); const duration = calculateDuration(trace.startTime, trace.endTime); const status = trace.status?.code === 0 ? "OK" : `Error: ${trace.status?.message || "Unknown error"}`; markdown += `| ${traceId} | ${displayName} | ${startTimeStr} | ${duration} | ${status} |\n`; } return { content: [ { type: "text", text: markdown, }, ], }; } catch (error: any) { // Error handling for natural-language-trace-query tool throw new GcpMcpError( `Failed to process natural language query: ${error.message}`, error.code || "UNKNOWN", error.statusCode || 500, ); } }, );
- src/services/trace/tools.ts:759-769 (schema)Zod input schema defining 'query' (required natural language string) and optional 'projectId'.{ query: z .string() .describe( 'Natural language query about traces (e.g., "Show me failed traces from the last hour")', ), projectId: z .string() .optional() .describe("Optional Google Cloud project ID"), },
- src/services/trace/types.ts:406-769 (helper)Helper to build hierarchical trace structure from raw API spans, used when fetching individual traces.export function buildTraceHierarchy( projectId: string, traceId: string, spans: any[], ): TraceData { // Log the raw spans for debugging logger.debug( `Building trace hierarchy for trace ${traceId} with ${spans.length} spans`, ); // Debug: Log the structure of the first span to understand the format if (spans.length > 0) { logger.debug(`First span structure: ${JSON.stringify(spans[0], null, 2)}`); logger.debug(`First span keys: ${Object.keys(spans[0]).join(", ")}`); } // We're using the v1 API which returns spans in a consistent format // Map to store spans by ID for quick lookup const spanMap = new Map<string, TraceSpan>(); // Convert raw spans to our TraceSpan format logger.debug("Converting spans to TraceSpan format..."); const traceSpans: TraceSpan[] = spans.map((span, index) => { // Extract span data - ensure we have a valid spanId const spanId = span.spanId || ""; logger.debug(`Processing span ${index} with ID: ${spanId}`); // Handle different display name formats in v1 API // According to https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces#TraceSpan let displayName = "Unknown Span"; // Debug: Log the name-related fields logger.debug(`Span ${spanId} name fields:`); logger.debug(`- name: ${span.name || "undefined"}`); logger.debug( `- displayName: ${typeof span.displayName === "object" ? JSON.stringify(span.displayName) : span.displayName || "undefined"}`, ); // In v1 API, the name field is the actual span name if (span.name) { displayName = span.name; logger.debug(`Using span.name: ${displayName}`); } else if (span.displayName?.value) { displayName = span.displayName.value; logger.debug(`Using span.displayName.value: ${displayName}`); } else if (typeof span.displayName === "string") { displayName = span.displayName; logger.debug(`Using span.displayName string: ${displayName}`); } // For v1 API, check if the name is a full URL path (common in Google Cloud Trace) if (displayName.startsWith("/")) { // This is likely an HTTP path displayName = `HTTP ${displayName}`; } // If we still have an unknown span, try to extract a meaningful name from labels if (displayName === "Unknown Span") { // Try to extract from common label patterns in Google Cloud Trace v1 API if (span.labels) { // Common Google Cloud Trace labels if (span.labels["/http/path"]) { const method = span.labels["/http/method"] || ""; displayName = `${method} ${span.labels["/http/path"]}`; } else if (span.labels["/http/url"]) { displayName = `HTTP ${span.labels["/http/url"]}`; } else if (span.labels["/http/status_code"]) { displayName = `HTTP Status: ${span.labels["/http/status_code"]}`; } else if (span.labels["/component"]) { displayName = `Component: ${span.labels["/component"]}`; } else if (span.labels["/db/statement"]) { const dbSystem = span.labels["/db/system"] || "DB"; displayName = `${dbSystem}: ${span.labels["/db/statement"].substring(0, 30)}...`; } else if (span.labels["g.co/agent"]) { displayName = `Agent: ${span.labels["g.co/agent"]}`; } else if (span.labels["g.co/gae/app/module"]) { displayName = `GAE Module: ${span.labels["g.co/gae/app/module"]}`; } else if (span.labels["g.co/gae/app/version"]) { displayName = `GAE Version: ${span.labels["g.co/gae/app/version"]}`; } else if (span.labels["g.co/gce/instance_id"]) { displayName = `GCE Instance: ${span.labels["g.co/gce/instance_id"]}`; } // If still unknown, check for any label that might be descriptive if (displayName === "Unknown Span") { // Look for descriptive labels const descriptiveLabels = Object.entries(span.labels).filter( ([key, value]) => typeof value === "string" && !key.startsWith("/") && !key.startsWith("g.co/") && value.length < 50, ); if (descriptiveLabels.length > 0) { // Use the first descriptive label const [key, value] = descriptiveLabels[0]; displayName = `${key}: ${value}`; } } } // Try alternative fields from the span object if (displayName === "Unknown Span") { // Check for operation name (common in Cloud Trace) if (span.operation && span.operation.name) { displayName = `Operation: ${span.operation.name}`; } // Check for any other descriptive fields const possibleNameFields = [ "operationName", "description", "type", "method", "rpcName", "kind", ]; for (const field of possibleNameFields) { if (span[field] && typeof span[field] === "string") { displayName = `${field}: ${span[field]}`; break; } } } } // If we still have an unknown span, include the span ID in the display name if (displayName === "Unknown Span" && spanId) { displayName = `Unknown Span (ID: ${spanId})`; } // Extract timestamps - in v1 API these are RFC3339 strings const startTime = span.startTime || ""; const endTime = span.endTime || ""; const parentSpanId = span.parentSpanId || ""; // Extract span kind - in v1 API, this might be in different formats let kind = "UNSPECIFIED"; if (span.kind) { kind = span.kind; } else if (span.spanKind) { kind = span.spanKind; } // In v1 API, the span kind might be encoded in labels if (kind === "UNSPECIFIED" && span.labels) { if (span.labels["/span/kind"]) { kind = span.labels["/span/kind"]; } else if (span.labels["span.kind"]) { kind = span.labels["span.kind"]; } } // Extract status - in v1 API this might be in different formats let status = TraceStatus.UNSPECIFIED; if (span.status) { if (span.status.code === 0) { status = TraceStatus.OK; } else if (span.status.code > 0) { status = TraceStatus.ERROR; } } // In v1 API, error status might be in labels if (status === TraceStatus.UNSPECIFIED && span.labels) { if (span.labels["/error/message"] || span.labels["error"]) { status = TraceStatus.ERROR; } else if (span.labels["/http/status_code"]) { const statusCode = parseInt(span.labels["/http/status_code"], 10); if (statusCode >= 400) { status = TraceStatus.ERROR; } else if (statusCode >= 200 && statusCode < 400) { status = TraceStatus.OK; } } } // Extract attributes/labels - handle both v1 and v2 formats const attributes: Record<string, string> = {}; // Debug: Log label information logger.debug(`Span ${spanId} labels:`); logger.debug(`- Has labels: ${!!span.labels}`); if (span.labels) { logger.debug(`- Label keys: ${Object.keys(span.labels).join(", ")}`); } // Handle v1 API format (labels) if (span.labels) { logger.debug( `Processing ${Object.keys(span.labels).length} labels for span ${spanId}`, ); for (const [key, value] of Object.entries(span.labels)) { if (value !== undefined && value !== null) { attributes[key] = String(value); } } } // Also handle v2 API format (attributes.attributeMap) if (span.attributes && span.attributes.attributeMap) { for (const [key, value] of Object.entries(span.attributes.attributeMap)) { if (value !== undefined && value !== null) { attributes[key] = (value as any)?.stringValue || String((value as any)?.intValue || (value as any)?.boolValue || ""); } } } // Handle any other fields that might contain useful information for (const [key, value] of Object.entries(span)) { // Skip keys we've already processed or that are part of the standard span structure if ( [ "spanId", "name", "displayName", "startTime", "endTime", "parentSpanId", "kind", "status", "labels", "attributes", "childSpans", ].includes(key) ) { continue; } // Add any other fields as attributes if (value !== undefined && value !== null) { if (typeof value !== "object") { attributes[`raw.${key}`] = String(value); } else if (!Array.isArray(value)) { // For simple objects, flatten one level try { attributes[`raw.${key}`] = JSON.stringify(value).substring(0, 100); if (JSON.stringify(value).length > 100) { attributes[`raw.${key}`] += "..."; } } catch (e) { // If we can't stringify, just note that it exists attributes[`raw.${key}`] = "[Complex Object]"; } } } } // Create the trace span const traceSpan: TraceSpan = { spanId, displayName, startTime, endTime, kind, status, attributes, childSpans: [], }; if (parentSpanId) { traceSpan.parentSpanId = parentSpanId; logger.debug(`Span ${spanId} has parent: ${parentSpanId}`); } else { logger.debug(`Span ${spanId} has no parent (will be a root span)`); } // Debug: Log the final display name logger.debug(`Final display name for span ${spanId}: "${displayName}"`); // Store in map for quick lookup spanMap.set(spanId, traceSpan); return traceSpan; }); // Build the hierarchy const rootSpans: TraceSpan[] = []; logger.debug(`Building trace hierarchy for ${traceSpans.length} spans...`); logger.debug(`Span map contains ${spanMap.size} spans`); for (const span of traceSpans) { logger.debug( `Processing hierarchy for span ${span.spanId} (${span.displayName})`, ); if (span.parentSpanId) { logger.debug(`Span ${span.spanId} has parent ${span.parentSpanId}`); // This is a child span, add it to its parent const parentSpan = spanMap.get(span.parentSpanId); if (parentSpan) { logger.debug( `Found parent span ${span.parentSpanId} for child ${span.spanId}`, ); if (!parentSpan.childSpans) { parentSpan.childSpans = []; logger.debug( `Initialized childSpans array for parent ${span.parentSpanId}`, ); } parentSpan.childSpans.push(span); logger.debug( `Added span ${span.spanId} as child of ${span.parentSpanId}`, ); } else { // Parent not found, treat as root logger.debug( `Parent ${span.parentSpanId} not found for span ${span.spanId}, treating as root`, ); rootSpans.push(span); } } else { // This is a root span logger.debug(`Span ${span.spanId} has no parent, adding as root span`); rootSpans.push(span); } } // Sort child spans by start time for (const span of traceSpans) { if (span.childSpans && span.childSpans.length > 0) { logger.debug( `Sorting ${span.childSpans.length} child spans for parent ${span.spanId}`, ); span.childSpans.sort((a, b) => { return ( new Date(a.startTime).getTime() - new Date(b.startTime).getTime() ); }); } } // Debug: Log the root spans and their children logger.debug(`Final hierarchy: ${rootSpans.length} root spans`); for (const rootSpan of rootSpans) { logger.debug(`Root span: ${rootSpan.spanId} (${rootSpan.displayName})`); logger.debug(`- Has ${rootSpan.childSpans?.length || 0} direct children`); // Count total descendants let totalDescendants = 0; const countDescendants = (span: TraceSpan) => { if (span.childSpans) { totalDescendants += span.childSpans.length; for (const child of span.childSpans) { countDescendants(child); } } }; countDescendants(rootSpan); logger.debug(`- Total descendants: ${totalDescendants}`); } return { traceId, projectId, rootSpans, allSpans: traceSpans, }; }
- src/services/trace/types.ts:113-166 (helper)Helper to format hierarchical trace data into detailed Markdown output.export function formatTraceData(traceData: TraceData): string { let markdown = `## Trace Details\n\n`; markdown += `- **Trace ID**: ${traceData.traceId}\n`; markdown += `- **Project ID**: ${traceData.projectId}\n`; markdown += `- **Total Spans**: ${traceData.allSpans.length}\n`; markdown += `- **Associated Logs**: [View logs for this trace](gcp-trace://${traceData.projectId}/traces/${traceData.traceId}/logs)\n\n`; // Add a summary of span types if we have them const spanTypes = new Map<string, number>(); traceData.allSpans.forEach((span) => { if (span.kind && span.kind !== "UNSPECIFIED") { spanTypes.set(span.kind, (spanTypes.get(span.kind) || 0) + 1); } }); if (spanTypes.size > 0) { markdown += `- **Span Types**:\n`; for (const [type, count] of spanTypes.entries()) { markdown += ` - ${type}: ${count}\n`; } markdown += `\n`; } // Format root spans and their children markdown += `## Trace Hierarchy\n\n`; for (const rootSpan of traceData.rootSpans) { markdown += formatSpanHierarchy(rootSpan, 0); } // Add section for failed spans if any const failedSpans = traceData.allSpans.filter( (span) => span.status === TraceStatus.ERROR, ); if (failedSpans.length > 0) { markdown += `\n## Failed Spans (${failedSpans.length})\n\n`; for (const span of failedSpans) { markdown += `- **${span.displayName}** (${span.spanId})\n`; markdown += ` - Start: ${new Date(span.startTime).toISOString()}\n`; markdown += ` - End: ${new Date(span.endTime).toISOString()}\n`; markdown += ` - Duration: ${calculateDuration(span.startTime, span.endTime)}\n`; // Add error details if available if (span.attributes["error.message"]) { markdown += ` - Error: ${span.attributes["error.message"]}\n`; } markdown += "\n"; } } return markdown; }
- src/services/trace/types.ts:777-812 (helper)Helper to extract trace IDs from log entries, used when querying traces via logs.export function extractTraceIdFromLog(logEntry: any): string | undefined { // Check for trace in the standard logging.googleapis.com/trace field if (logEntry.trace) { // The trace field is typically in the format "projects/PROJECT_ID/traces/TRACE_ID" const match = logEntry.trace.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } // Check for trace in labels if (logEntry.labels && logEntry.labels["logging.googleapis.com/trace"]) { const traceLabel = logEntry.labels["logging.googleapis.com/trace"]; const match = traceLabel.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } // Check for trace in jsonPayload if (logEntry.jsonPayload) { if (logEntry.jsonPayload.traceId) { return logEntry.jsonPayload.traceId; } if (logEntry.jsonPayload["logging.googleapis.com/trace"]) { const tracePayload = logEntry.jsonPayload["logging.googleapis.com/trace"]; const match = tracePayload.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } } return undefined; }