From 539b1701dd38faa0ec96de9920b5eb11e9dc86a8 Mon Sep 17 00:00:00 2001 From: JeremyFunk Date: Thu, 15 Oct 2026 19:31:28 +0200 Subject: [PATCH] feat(service-map): show Google Cloud as the platform for services hosted on GCP Google Cloud's resource detector sets faas.name on Cloud Run and Cloud Functions, so those services were drawn as AWS Lambda, and GKE services as plain Kubernetes. cloud.provider=gcp now classifies as a gcp platform with the Google Cloud mark, tooltip, colour and legend entry. --- .../src/routes/internal/query-engine.http.ts | 22 +++++++++++-------- .../src/hooks/use-local-service-map.test.ts | 15 +++++++++++++ .../src/hooks/use-local-service-map.ts | 2 ++ apps/web/src/api/warehouse/service-map.ts | 2 +- apps/web/src/components/icons/index.ts | 3 ++- apps/web/src/lab/bench/service-map-bench.tsx | 2 +- packages/domain/src/http/query-engine.ts | 9 +++++++- .../ui}/src/components/icons/google-cloud.tsx | 0 packages/ui/src/components/icons/index.ts | 1 + .../service-map/service-map-canvas.tsx | 13 ++++++++--- .../service-map/service-map-node.tsx | 3 +++ .../service-map/service-map-types.ts | 2 +- .../service-map/service-map-utils.ts | 1 + 13 files changed, 58 insertions(+), 17 deletions(-) rename {apps/web => packages/ui}/src/components/icons/google-cloud.tsx (100%) 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)",