Skip to main content
Glama
krzko

Google Cloud MCP Server

by krzko

gcp-trace-get-trace

Retrieve specific trace data from Google Cloud Trace by providing a trace ID to analyze request performance and debug distributed systems.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
traceIdYesThe trace ID to retrieve
projectIdNoOptional Google Cloud project ID

Implementation Reference

  • Main tool handler: fetches trace by ID from GCP Cloud Trace API v1 using REST API, processes spans, builds hierarchical structure, formats as Markdown, handles auth and errors.
    async ({ traceId, projectId }, context) => {
      try {
        // Use provided project ID or get the default one from state manager first
        const actualProjectId =
          projectId ||
          stateManager.getCurrentProjectId() ||
          (await getProjectId());
    
        // 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 v1
        // API Reference: https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces/get
        const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`;
        logger.debug(`Fetching trace from: ${apiUrl}`);
        const response = await fetch(apiUrl, {
          method: "GET",
          headers: {
            Authorization: `Bearer ${token.token}`,
            Accept: "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();
    
        // Log the raw trace data for debugging
        logger.debug(`Raw trace data: ${JSON.stringify(traceData, null, 2)}`);
    
        // Debug: Log the exact structure of the trace data
        logger.debug("Trace data structure:");
        logger.debug(`- Type: ${typeof traceData}`);
        logger.debug(`- Keys: ${Object.keys(traceData).join(", ")}`);
        logger.debug(`- Has spans array: ${Array.isArray(traceData.spans)}`);
        logger.debug(`- Spans array length: ${traceData.spans?.length || 0}`);
    
        // Check if we have valid trace data
        // In v1 API, the response is a Trace object with spans array
        if (!traceData || !traceData.spans || traceData.spans.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`,
              },
            ],
          };
        }
    
        // Log the trace structure for debugging
        logger.debug(
          `Trace structure: projectId=${traceData.projectId}, traceId=${traceData.traceId}, spans count=${traceData.spans.length}`,
        );
    
        // Log the first span to help with debugging
        if (traceData.spans && traceData.spans.length > 0) {
          const firstSpan = traceData.spans[0];
          logger.debug(
            `First span example: ${JSON.stringify(firstSpan, null, 2)}`,
          );
          logger.debug(
            `First span fields: ${Object.keys(firstSpan).join(", ")}`,
          );
    
          // Debug: Log specific fields that we're looking for in the span
          logger.debug("Span field details:");
          logger.debug(`- spanId: ${firstSpan.spanId}`);
          logger.debug(`- name: ${firstSpan.name}`);
          logger.debug(`- displayName: ${firstSpan.displayName}`);
          logger.debug(`- startTime: ${firstSpan.startTime}`);
          logger.debug(`- endTime: ${firstSpan.endTime}`);
          logger.debug(`- parentSpanId: ${firstSpan.parentSpanId}`);
          logger.debug(`- kind: ${firstSpan.kind}`);
          logger.debug(`- Has labels: ${!!firstSpan.labels}`);
    
          if (firstSpan.labels) {
            logger.debug(
              `- Label keys: ${Object.keys(firstSpan.labels).join(", ")}`,
            );
            logger.debug(
              `- HTTP path label: ${firstSpan.labels["/http/path"]}`,
            );
            logger.debug(
              `- HTTP method label: ${firstSpan.labels["/http/method"]}`,
            );
            logger.debug(
              `- Component label: ${firstSpan.labels["/component"]}`,
            );
          }
        }
    
        // Add additional metadata to the response for better context
        let responseText = `\`\`\`json\n${JSON.stringify(
          {
            traceId: traceId,
            projectId: actualProjectId,
            spanCount: traceData.spans.length,
          },
          null,
          2,
        )}\n\`\`\`\n`;
    
        // Log the number of spans found
        logger.debug(
          `Found ${traceData.spans.length} spans in trace ${traceId}`,
        );
    
        try {
          logger.debug("Starting to build trace hierarchy...");
    
          // Debug: Log each span before processing
          traceData.spans.forEach((span: any, index: number) => {
            logger.debug(`Span ${index} (ID: ${span.spanId}):`);
            logger.debug(`- Name: ${span.name || "undefined"}`);
            logger.debug(`- Parent: ${span.parentSpanId || "None"}`);
            logger.debug(`- Has labels: ${!!span.labels}`);
            if (span.labels) {
              logger.debug(`- Label count: ${Object.keys(span.labels).length}`);
            }
          });
    
          // Build the trace hierarchy
          const hierarchicalTrace = buildTraceHierarchy(
            actualProjectId.toString(),
            traceId.toString(),
            traceData.spans,
          );
    
          logger.debug("Trace hierarchy built successfully");
          logger.debug(
            `Root spans count: ${hierarchicalTrace.rootSpans.length}`,
          );
    
          // Format the trace data for display
          logger.debug("Formatting trace data...");
          const formattedTrace = formatTraceData(hierarchicalTrace);
    
          // Combine the response text with the formatted trace
          responseText += formattedTrace;
    
          logger.debug("Trace formatting complete");
        } catch (hierarchyError: any) {
          // If we encounter an error building the hierarchy, log it and provide raw span info
          logger.error(
            `Error building trace hierarchy: ${hierarchyError.message}`,
          );
    
          // Provide a simplified trace summary
          responseText += "## Error Building Trace Hierarchy\n\n";
          responseText += `Error: ${hierarchyError.message}\n\n`;
          responseText += "## Raw Span Summary\n\n";
    
          // List spans with basic information
          for (const span of traceData.spans) {
            const spanId = span.spanId || "Unknown";
            const name = span.name || "Unknown";
            const parentId = span.parentSpanId || "None";
    
            responseText += `- **Span ID**: ${spanId}\n`;
            responseText += `  - Name: ${name}\n`;
            responseText += `  - Parent: ${parentId}\n`;
    
            // Add timing if available
            if (span.startTime && span.endTime) {
              const startDate = new Date(span.startTime);
              const endDate = new Date(span.endTime);
              const durationMs = endDate.getTime() - startDate.getTime();
              responseText += `  - Duration: ${durationMs}ms\n`;
            }
    
            // Add a few important labels if available
            if (span.labels) {
              responseText += `  - Labels: ${Object.keys(span.labels).length} total\n`;
              const importantLabels = [
                "/http/method",
                "/http/path",
                "/http/status_code",
                "/component",
                "g.co/agent",
              ];
              for (const key of importantLabels) {
                if (span.labels[key]) {
                  responseText += `    - ${key}: ${span.labels[key]}\n`;
                }
              }
            }
    
            responseText += "\n";
          }
        }
    
        return {
          content: [
            {
              type: "text",
              text: responseText,
            },
          ],
        };
      } catch (error: any) {
        // Error handling for get-trace tool
        throw new GcpMcpError(
          `Failed to fetch trace: ${error.message}`,
          error.code || "UNKNOWN",
          error.statusCode || 500,
        );
      }
    },
  • Input schema using Zod: traceId (required string), projectId (optional string).
    {
      traceId: z.string().describe("The trace ID to retrieve"),
      projectId: z
        .string()
        .optional()
        .describe("Optional Google Cloud project ID"),
    },
  • Tool registration within registerTraceTools function using McpServer.tool().
      "gcp-trace-get-trace",
      {
        traceId: z.string().describe("The trace ID to retrieve"),
        projectId: z
          .string()
          .optional()
          .describe("Optional Google Cloud project ID"),
      },
      async ({ traceId, projectId }, context) => {
        try {
          // Use provided project ID or get the default one from state manager first
          const actualProjectId =
            projectId ||
            stateManager.getCurrentProjectId() ||
            (await getProjectId());
    
          // 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 v1
          // API Reference: https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces/get
          const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`;
          logger.debug(`Fetching trace from: ${apiUrl}`);
          const response = await fetch(apiUrl, {
            method: "GET",
            headers: {
              Authorization: `Bearer ${token.token}`,
              Accept: "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();
    
          // Log the raw trace data for debugging
          logger.debug(`Raw trace data: ${JSON.stringify(traceData, null, 2)}`);
    
          // Debug: Log the exact structure of the trace data
          logger.debug("Trace data structure:");
          logger.debug(`- Type: ${typeof traceData}`);
          logger.debug(`- Keys: ${Object.keys(traceData).join(", ")}`);
          logger.debug(`- Has spans array: ${Array.isArray(traceData.spans)}`);
          logger.debug(`- Spans array length: ${traceData.spans?.length || 0}`);
    
          // Check if we have valid trace data
          // In v1 API, the response is a Trace object with spans array
          if (!traceData || !traceData.spans || traceData.spans.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`,
                },
              ],
            };
          }
    
          // Log the trace structure for debugging
          logger.debug(
            `Trace structure: projectId=${traceData.projectId}, traceId=${traceData.traceId}, spans count=${traceData.spans.length}`,
          );
    
          // Log the first span to help with debugging
          if (traceData.spans && traceData.spans.length > 0) {
            const firstSpan = traceData.spans[0];
            logger.debug(
              `First span example: ${JSON.stringify(firstSpan, null, 2)}`,
            );
            logger.debug(
              `First span fields: ${Object.keys(firstSpan).join(", ")}`,
            );
    
            // Debug: Log specific fields that we're looking for in the span
            logger.debug("Span field details:");
            logger.debug(`- spanId: ${firstSpan.spanId}`);
            logger.debug(`- name: ${firstSpan.name}`);
            logger.debug(`- displayName: ${firstSpan.displayName}`);
            logger.debug(`- startTime: ${firstSpan.startTime}`);
            logger.debug(`- endTime: ${firstSpan.endTime}`);
            logger.debug(`- parentSpanId: ${firstSpan.parentSpanId}`);
            logger.debug(`- kind: ${firstSpan.kind}`);
            logger.debug(`- Has labels: ${!!firstSpan.labels}`);
    
            if (firstSpan.labels) {
              logger.debug(
                `- Label keys: ${Object.keys(firstSpan.labels).join(", ")}`,
              );
              logger.debug(
                `- HTTP path label: ${firstSpan.labels["/http/path"]}`,
              );
              logger.debug(
                `- HTTP method label: ${firstSpan.labels["/http/method"]}`,
              );
              logger.debug(
                `- Component label: ${firstSpan.labels["/component"]}`,
              );
            }
          }
    
          // Add additional metadata to the response for better context
          let responseText = `\`\`\`json\n${JSON.stringify(
            {
              traceId: traceId,
              projectId: actualProjectId,
              spanCount: traceData.spans.length,
            },
            null,
            2,
          )}\n\`\`\`\n`;
    
          // Log the number of spans found
          logger.debug(
            `Found ${traceData.spans.length} spans in trace ${traceId}`,
          );
    
          try {
            logger.debug("Starting to build trace hierarchy...");
    
            // Debug: Log each span before processing
            traceData.spans.forEach((span: any, index: number) => {
              logger.debug(`Span ${index} (ID: ${span.spanId}):`);
              logger.debug(`- Name: ${span.name || "undefined"}`);
              logger.debug(`- Parent: ${span.parentSpanId || "None"}`);
              logger.debug(`- Has labels: ${!!span.labels}`);
              if (span.labels) {
                logger.debug(`- Label count: ${Object.keys(span.labels).length}`);
              }
            });
    
            // Build the trace hierarchy
            const hierarchicalTrace = buildTraceHierarchy(
              actualProjectId.toString(),
              traceId.toString(),
              traceData.spans,
            );
    
            logger.debug("Trace hierarchy built successfully");
            logger.debug(
              `Root spans count: ${hierarchicalTrace.rootSpans.length}`,
            );
    
            // Format the trace data for display
            logger.debug("Formatting trace data...");
            const formattedTrace = formatTraceData(hierarchicalTrace);
    
            // Combine the response text with the formatted trace
            responseText += formattedTrace;
    
            logger.debug("Trace formatting complete");
          } catch (hierarchyError: any) {
            // If we encounter an error building the hierarchy, log it and provide raw span info
            logger.error(
              `Error building trace hierarchy: ${hierarchyError.message}`,
            );
    
            // Provide a simplified trace summary
            responseText += "## Error Building Trace Hierarchy\n\n";
            responseText += `Error: ${hierarchyError.message}\n\n`;
            responseText += "## Raw Span Summary\n\n";
    
            // List spans with basic information
            for (const span of traceData.spans) {
              const spanId = span.spanId || "Unknown";
              const name = span.name || "Unknown";
              const parentId = span.parentSpanId || "None";
    
              responseText += `- **Span ID**: ${spanId}\n`;
              responseText += `  - Name: ${name}\n`;
              responseText += `  - Parent: ${parentId}\n`;
    
              // Add timing if available
              if (span.startTime && span.endTime) {
                const startDate = new Date(span.startTime);
                const endDate = new Date(span.endTime);
                const durationMs = endDate.getTime() - startDate.getTime();
                responseText += `  - Duration: ${durationMs}ms\n`;
              }
    
              // Add a few important labels if available
              if (span.labels) {
                responseText += `  - Labels: ${Object.keys(span.labels).length} total\n`;
                const importantLabels = [
                  "/http/method",
                  "/http/path",
                  "/http/status_code",
                  "/component",
                  "g.co/agent",
                ];
                for (const key of importantLabels) {
                  if (span.labels[key]) {
                    responseText += `    - ${key}: ${span.labels[key]}\n`;
                  }
                }
              }
    
              responseText += "\n";
            }
          }
    
          return {
            content: [
              {
                type: "text",
                text: responseText,
              },
            ],
          };
        } catch (error: any) {
          // Error handling for get-trace tool
          throw new GcpMcpError(
            `Failed to fetch trace: ${error.message}`,
            error.code || "UNKNOWN",
            error.statusCode || 500,
          );
        }
      },
    );
  • Helper to build hierarchical trace structure from raw API spans: creates TraceSpan objects, links children via parentSpanId, sorts by time.
    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 TraceData into human-readable Markdown with hierarchy, timings, attributes, status indicators.
    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;
    }

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