Skip to main content
Glama
krzko

Google Cloud MCP Server

by krzko

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
NameRequiredDescriptionDefault
projectIdNoOptional Google Cloud project ID
queryYesNatural language query about traces (e.g., "Show me failed traces from the last hour")

Implementation Reference

  • 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, ); } }, );
  • 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"), },
  • 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, }; }
  • 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; }
  • 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; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/krzko/google-cloud-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server