diff --git a/apps/api/src/routes/internal/query-engine.http.ts b/apps/api/src/routes/internal/query-engine.http.ts index 6bb4078732..2af9d78b04 100644 --- a/apps/api/src/routes/internal/query-engine.http.ts +++ b/apps/api/src/routes/internal/query-engine.http.ts @@ -254,17 +254,21 @@ const toServicePlatformRow = (row: CH.ServicePlatformsOutput) => { const processRuntimeName = row.processRuntimeName // cluster.name alone does not prove the service runs in Kubernetes. const isKubernetes = k8sPodName !== "" || k8sDeploymentName !== "" - // Host infrastructure takes precedence over SDK self-report. - const platform: "kubernetes" | "cloudflare" | "lambda" | "web" | "unknown" = + // Host infrastructure takes precedence over SDK self-report. Google Cloud's + // detector sets `faas.name` on Cloud Run and Cloud Functions, so it is checked + // before the Lambda rule. + const platform: "kubernetes" | "cloudflare" | "gcp" | "lambda" | "web" | "unknown" = cloudPlatform === "cloudflare.workers" || cloudProvider === "cloudflare" ? "cloudflare" - : faasName !== "" || cloudPlatform === "aws_lambda" - ? "lambda" - : isKubernetes - ? "kubernetes" - : mapleSdkType === "client" - ? "web" - : "unknown" + : cloudProvider === "gcp" + ? "gcp" + : faasName !== "" || cloudPlatform === "aws_lambda" + ? "lambda" + : isKubernetes + ? "kubernetes" + : mapleSdkType === "client" + ? "web" + : "unknown" return { serviceName: decodeServiceName(row.serviceName), platform, diff --git a/apps/local-ui/src/hooks/use-local-service-map.test.ts b/apps/local-ui/src/hooks/use-local-service-map.test.ts index 50de28f87a..e6cfdb772a 100644 --- a/apps/local-ui/src/hooks/use-local-service-map.test.ts +++ b/apps/local-ui/src/hooks/use-local-service-map.test.ts @@ -82,6 +82,21 @@ describe("classifyPlatform", () => { "cloudflare", ) expect(classifyPlatform(platformRow({ faasName: "fn", k8sPodName: "pod" }))).toBe("lambda") + // Cloud Run and GKE: Google Cloud's detector also sets `faas.name` / the k8s attributes. + expect( + classifyPlatform( + platformRow({ cloudProvider: "gcp", cloudPlatform: "gcp_cloud_run", faasName: "checkout" }), + ), + ).toBe("gcp") + expect( + classifyPlatform( + platformRow({ + cloudProvider: "gcp", + cloudPlatform: "gcp_kubernetes_engine", + k8sPodName: "pod", + }), + ), + ).toBe("gcp") expect(classifyPlatform(platformRow({ k8sDeploymentName: "api" }))).toBe("kubernetes") expect(classifyPlatform(platformRow({ mapleSdkType: "client" }))).toBe("web") }) diff --git a/apps/local-ui/src/hooks/use-local-service-map.ts b/apps/local-ui/src/hooks/use-local-service-map.ts index 7677a27195..e12a4a43f6 100644 --- a/apps/local-ui/src/hooks/use-local-service-map.ts +++ b/apps/local-ui/src/hooks/use-local-service-map.ts @@ -107,6 +107,8 @@ function toDbEdge(row: CH.ServiceDbEdgesOutput, durationSeconds: number): Servic /** Same precedence as the cloud API's platform classifier: host infrastructure beats SDK self-report. */ export function classifyPlatform(row: CH.ServicePlatformsOutput): ServicePlatform { if (row.cloudPlatform === "cloudflare.workers" || row.cloudProvider === "cloudflare") return "cloudflare" + // Before the Lambda rule: Google Cloud's detector sets `faas.name` on Cloud Run and Cloud Functions. + if (row.cloudProvider === "gcp") return "gcp" if (row.faasName !== "" || row.cloudPlatform === "aws_lambda") return "lambda" // `k8s.cluster.name` alone does not prove the service runs in Kubernetes. if (row.k8sPodName !== "" || row.k8sDeploymentName !== "") return "kubernetes" diff --git a/apps/web/src/api/warehouse/service-map.ts b/apps/web/src/api/warehouse/service-map.ts index bce16feaae..173fb283c6 100644 --- a/apps/web/src/api/warehouse/service-map.ts +++ b/apps/web/src/api/warehouse/service-map.ts @@ -96,7 +96,7 @@ export interface ServiceDbQuerySummaryResponse { topQueries: ReadonlyArray } -export type ServicePlatform = "kubernetes" | "cloudflare" | "lambda" | "web" | "unknown" +export type ServicePlatform = "kubernetes" | "cloudflare" | "gcp" | "lambda" | "web" | "unknown" const GetServiceMapInputSchema = Schema.Struct({ startTime: Schema.optional(WarehouseDateTimeString), diff --git a/apps/web/src/components/icons/index.ts b/apps/web/src/components/icons/index.ts index 459385827d..c47c48e3c9 100644 --- a/apps/web/src/components/icons/index.ts +++ b/apps/web/src/components/icons/index.ts @@ -42,6 +42,8 @@ export { FolderOpenIcon, GlobeIcon, GlobePointerIcon, + GoogleCloudIcon, + GoogleCloudMonoIcon, KubernetesIcon, LineHeightIcon, LinkIcon, @@ -137,7 +139,6 @@ export { GearIcon } from "./gear" export { GeminiIcon } from "./gemini" export { GithubIcon } from "./github" export { GoogleIcon } from "./google" -export { GoogleCloudIcon, GoogleCloudMonoIcon } from "./google-cloud" export { GrokIcon } from "./grok" export { HaystackIcon } from "./haystack" export { HuggingFaceIcon } from "./huggingface" diff --git a/apps/web/src/lab/bench/service-map-bench.tsx b/apps/web/src/lab/bench/service-map-bench.tsx index 9fc5a533a4..f61c5d90de 100644 --- a/apps/web/src/lab/bench/service-map-bench.tsx +++ b/apps/web/src/lab/bench/service-map-bench.tsx @@ -74,7 +74,7 @@ function makeRng(seed: number): () => number { } } -const PLATFORMS: ServicePlatform[] = ["kubernetes", "cloudflare", "lambda", "web", "unknown"] +const PLATFORMS: ServicePlatform[] = ["kubernetes", "cloudflare", "gcp", "lambda", "web", "unknown"] const RUNTIMES = ["nodejs", "bun", "deno", "edge-light", "workerd"] const DB_SYSTEMS = ["postgresql", "mysql", "clickhouse", "redis", "mongodb"] diff --git a/packages/domain/src/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index 0eb392759f..0b91f29579 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -997,7 +997,14 @@ export class ServiceDbQuerySummaryResponse extends Schema.Class("ServicePlatformsRequest")( { diff --git a/apps/web/src/components/icons/google-cloud.tsx b/packages/ui/src/components/icons/google-cloud.tsx similarity index 100% rename from apps/web/src/components/icons/google-cloud.tsx rename to packages/ui/src/components/icons/google-cloud.tsx diff --git a/packages/ui/src/components/icons/index.ts b/packages/ui/src/components/icons/index.ts index 159b0caa61..325cc09279 100644 --- a/packages/ui/src/components/icons/index.ts +++ b/packages/ui/src/components/icons/index.ts @@ -56,6 +56,7 @@ export { FireIcon } from "./fire" export { FolderOpenIcon } from "./folder-open" export { GaugeIcon } from "./gauge" export { GlobeIcon } from "./globe" +export { GoogleCloudIcon, GoogleCloudMonoIcon } from "./google-cloud" export { GlobePointerIcon } from "./globe-pointer" export { IndustryIcon } from "./industry" export { KubernetesIcon } from "./kubernetes" diff --git a/packages/ui/src/components/service-map/service-map-canvas.tsx b/packages/ui/src/components/service-map/service-map-canvas.tsx index 66cb16a559..9c11543b38 100644 --- a/packages/ui/src/components/service-map/service-map-canvas.tsx +++ b/packages/ui/src/components/service-map/service-map-canvas.tsx @@ -974,7 +974,14 @@ export function ServiceMapFlowCanvas({ ) } -const LEGEND_PLATFORMS = ["kubernetes", "cloudflare", "lambda", "web", "unknown"] as const +const LEGEND_PLATFORMS = [ + ["kubernetes", "Kubernetes"], + ["cloudflare", "Cloudflare"], + ["gcp", "Google Cloud"], + ["lambda", "Lambda"], + ["web", "Web"], + ["unknown", "Unknown"], +] as const const serviceSwatchColor = (service: string) => getServiceMapNodeColor({ label: service, kind: "service", errorRate: 0 }, "service") @@ -1025,13 +1032,13 @@ function ServiceMapLegend({ colorMode, services }: { colorMode: ServiceMapColorM {colorMode === "platform" && ( <> | - {LEGEND_PLATFORMS.map((p) => ( + {LEGEND_PLATFORMS.map(([p, label]) => (
- {p} + {label}
))} diff --git a/packages/ui/src/components/service-map/service-map-node.tsx b/packages/ui/src/components/service-map/service-map-node.tsx index 76e8468fc2..7fcdaf1cc1 100644 --- a/packages/ui/src/components/service-map/service-map-node.tsx +++ b/packages/ui/src/components/service-map/service-map-node.tsx @@ -15,6 +15,7 @@ import { CloudflareBrandIcon, CubeIcon, GlobeIcon, + GoogleCloudIcon, type IconComponent, KubernetesIcon, ServerIcon, @@ -34,6 +35,8 @@ function getPlatformIcon(platform: ServicePlatform | undefined): { return { Icon: KubernetesIcon, label: "Kubernetes", branded: true } case "cloudflare": return { Icon: CloudflareBrandIcon, label: "Cloudflare Workers", branded: true } + case "gcp": + return { Icon: GoogleCloudIcon, label: "Google Cloud", branded: true } case "lambda": return { Icon: AwsLambdaIcon, label: "AWS Lambda", branded: true } case "web": diff --git a/packages/ui/src/components/service-map/service-map-types.ts b/packages/ui/src/components/service-map/service-map-types.ts index 83009b7bbb..ae1d4fa90f 100644 --- a/packages/ui/src/components/service-map/service-map-types.ts +++ b/packages/ui/src/components/service-map/service-map-types.ts @@ -2,7 +2,7 @@ // Local) maps its own query results onto these shapes, so the map itself never // learns which backend answered. -export type ServicePlatform = "kubernetes" | "cloudflare" | "lambda" | "web" | "unknown" +export type ServicePlatform = "kubernetes" | "cloudflare" | "gcp" | "lambda" | "web" | "unknown" /** One service-to-service edge over the window. */ export interface ServiceMapEdgeRow { diff --git a/packages/ui/src/components/service-map/service-map-utils.ts b/packages/ui/src/components/service-map/service-map-utils.ts index c7e97dfe08..8f40193178 100644 --- a/packages/ui/src/components/service-map/service-map-utils.ts +++ b/packages/ui/src/components/service-map/service-map-utils.ts @@ -114,6 +114,7 @@ export interface ServiceNodeData { const PLATFORM_COLORS: Record = { kubernetes: "oklch(0.62 0.16 250)", cloudflare: CLOUDFLARE_COLOR, + gcp: "oklch(0.63 0.2 27)", lambda: "oklch(0.7 0.18 60)", web: "oklch(0.65 0.15 145)", unknown: "oklch(0.55 0.02 270)",